Writing data
The LUXPOS API supports a small, deliberately conservative set of write operations. Each requires a dedicated :write scope and acts only on the business the API key belongs to.
Write endpoints
/public/productsproducts:write/public/products/:idproducts:write/public/products/:id/inventoryinventory:write/public/customerscustomers:write/public/customers/:idcustomers:write/public/invoicesinvoices:write/public/invoices/:idinvoices:write/public/invoices/:id/itemsinvoices:write/public/invoices/:id/items/:itemIdinvoices:write/public/invoices/:id/items/:itemIdinvoices:write/public/invoices/:id/finalizeinvoices:write/public/invoices/:idinvoices:write/public/invoices/:id/credit-noteinvoices:write/public/loyalty/cards/:id/stampsloyalty:write/public/loyalty/cards/:id/stamps/removeloyalty:write/public/loyalty/cards/:id/pointsloyalty:write/public/loyalty/cards/:id/redeemloyalty:write/public/loyalty/coupons/:code/redeemloyalty:writeIdempotency
Every POST write endpoint accepts an Idempotency-Key header. If a request times out or your client retries, send the same key and LUXPOS returns the original result instead of performing the action twice. Use a fresh UUID per logical operation.
POST /api/v1/public/products HTTP/1.1
Host: api.luxpos.lu
Authorization: Bearer lpk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Idempotency-Key: 7c1f0e2a-9b3d-4f6a-8c5e-1d2b3a4c5d6e
Content-Type: application/json
{
"name": "Hair Wax Matte Finish",
"sellingPrice": 18.99,
"vatPercent": 17,
"trackInventory": true,
"stockQuantity": 50
}- Keys are scoped to your API key — they never collide with another business.
- A concurrent retry of an in-flight key returns
409 Conflict. - Failed writes release the key so you can legitimately retry.
- Limitation: idempotency is a best-effort retry guard held in memory; it deduplicates retries within minutes, not across long gaps or node restarts. Design writes to be naturally safe (e.g. match on SKU) for absolute guarantees.
Adjusting stock
Stock is changed with a signed delta, not an absolute value, so concurrent adjustments compose correctly. Every adjustment writes an entry to the product's inventory audit log. Omit locationId for single-location businesses to adjust the product total; pass a branch id to adjust that branch (the total is kept in sync).
{
"locationId": "b1a2…", // optional
"delta": -3, // required, non-zero
"reason": "RECOUNT",
"note": "Quarterly stock take"
}An adjustment that would make stock negative is rejected with 400 Bad Request.
Creating customers
POST /public/customers creates a customer and links them to your business. If a person with the same email or phone already exists, they are linked rather than duplicated, and the response indicates isExisting: true. Updates via PATCH /public/customers/:id only succeed for customers that belong to your business — others return 404.
Multicurrency invoices
POST /public/invoices bills in EUR by default. To invoice a customer in a foreign currency, pass an ISO-4217 code (exactly three letters) in the currency field — for example "USD". It is normalized to upper-case for you.
{
"buyerName": "ACME Corp.",
"currency": "USD",
"items": [
{ "description": "Consulting — March", "quantity": 10, "unitPrice": 120 }
],
"finalize": true
}Conversion to EUR is automatic — your integration does nothing else. LUXPOS looks up the European Central Bank reference rate for the invoice date and stores it with the invoice. The response (and every later read) returns three FX fields:
currency— the invoice currency you sent.exchangeRate— the ECB reference rate applied on the invoice date (1for an EUR invoice).baseAmount— the EUR equivalent oftotalAmount, i.e. the figure used for accounting, VAT and reporting thresholds.
The generated invoice PDF prints both the foreign-currency total and the EUR equivalent, so the customer sees the amount they owe while your books stay in EUR. FX resolution is best-effort: if a rate is briefly unavailable the invoice is still created and the rate backfills, so never block your flow on these fields.
Invoice documents: printed references
Besides externalRef (stored for your reconciliation, never printed), an invoice or credit note can carry three optional references that are printed in the document header, each only when set:
buyerReference— the buyer's own order / dossier reference (e.g. their purchase-order number). Printed as Référence, so the buyer's AP department can match the document. Max 120 chars.customerNumber— your customer number for this buyer. Printed as N° client. Max 60 chars. OnPOST /public/invoices/:id/credit-noteit defaults to the credited invoice's value (same buyer) and can be overridden.claimReference— a claim / complaint file number. Printed as Réclamation N°. Typically passed when creating a credit note that settles a customer claim. Max 120 chars. Not inherited from the credited invoice.
{
"claimReference": "REC-2026-0042 · CPL: 12345",
"customerNumber": "K-10120",
"notes": "Avoir suite réclamation — 2 unités endommagées",
"finalize": true
}All three fields are accepted on POST /public/invoices, PATCH /public/invoices/:id (drafts only; send an empty string to clear) and POST /public/invoices/:id/credit-note, and are echoed back in every detail read so you can verify what will appear on the document.
PDF layout: the totals block ventilates VAT per rate including the taxable base (e.g. Base imposable 3 % / TVA 3 %) when the merchant's template uses per-rate VAT summary. The line-item columns are merchant-controlled in Dashboard → Accounting → Invoices → Template — including optional per-line Total HTVA (net, default on) and Total TTC (gross, default off) columns. Your integration sends the same data either way; only the rendering changes.
Security model
- The target business is always taken from the API key, never the request body.
- Every write verifies the resource (product, customer, location) belongs to that business before mutating it.
- Input is strictly validated; unknown fields are rejected and only a safe whitelist of fields is writable.
- Write endpoints share the same rate limit as reads (60 req/min). See Rate limits.