Keep an ERP in sync
This page is for a system that mirrors a merchant's documents, such as an ERP or an accounting package, and may also write to Kosoor.
Read what is new
Every list returns the newest records first, whichever channel created them: the POS, the kiosk, the online store, the dashboard or the API. A record created while you page appears at the top of the list, so it never shifts the pages you are reading. See Pagination.
So to fetch what is new since your last run:
- Keep the
idof the newest record you stored for each list. - Ask for the first page:
GET /v1/sales-invoices?limit=100. - Store each record until you meet the
idyou kept. Stop there. - If you did not meet it and
hasMoreistrue, ask for the next page withstartingAfterset to the lastidof this page, and go on from step 3. - Keep the
idof the first record of the first page for the next run.
curl "https://api.kosoor.com.sa/v1/sales-invoices?limit=100&startingAfter=c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f" \
-H "Authorization: Bearer ksr_live_..."
{
"data": [
{
"id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"object": "salesInvoice",
"number": "INV-1042",
"issueDate": "2026-11-07",
"supplyDate": null,
"dueDate": null,
"branchId": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
"customerId": "5a4db0c5-0000-4000-8000-000000000001",
"currency": "SAR",
"subtotal": 100,
"discountTotal": 0,
"vatTotal": 15,
"total": 115,
"amountPaid": 115,
"amountDue": 0,
"notes": null,
"costCentreId": null,
"referenceId": "order-20417",
"metadata": {},
"createdVia": "api",
"zatca": {
"kind": "simplified",
"status": "reported",
"uuid": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"counter": 17,
"hash": "dZBGdHmJoIQ0SOi+mt6KcGC6mWlVmpjbPEJ4y/3tps4=",
"qr": "ARBTYW5kYm94IE1lcmNoYW50Ag8zOTk5OTk5OTk5MDAwMDMDFDIwMjYtMTEtMDdUMDk6MzA6MTJaBAYxMTUuMDAFBTE1LjAw",
"submittedAt": "2026-11-07T09:30:40.118Z",
"clearedAt": null,
"reportedAt": "2026-11-07T09:30:41.204Z",
"errors": []
}
}
],
"hasMore": false
}
A list leaves out each invoice's lines. Add expand=lines to get them, and expand=customer to embed the customer (which also needs customers:read); repeat the parameter for both: expand=lines&expand=customer.
The same steps work for GET /v1/payments and GET /v1/credit-notes. The credit note list includes refunds made at the POS and on the dashboard.
To load history once, filter by date instead: issuedAfter and issuedBefore on sales invoices and credit notes (instants in ISO 8601, such as 2026-01-01T00:00:00Z), and dateFrom and dateTo on payments (days, such as 2026-01-01).
Tell the channels apart
createdVia says which channel created a record: api, dashboard, pos (the point of sale in a branch), kiosk, storefront (the merchant's online store), or unknown for a record saved before Kosoor recorded the channel. api covers every key the merchant issued, not only yours. To recognise your own records, give them your own referenceId, or a key in metadata.
Records that change after you stored them
A list shows each record once, when it is created. Some fields change later:
| Field | Changes when |
|---|---|
zatca.status and the other zatca fields | ZATCA answers: pending turns cleared, reported or rejected |
amountPaid, amountDue of an invoice | a payment is allocated to it, or a credit note takes an amount off it |
| any field of a customer | the merchant or a key edits the customer |
Read a record again with GET on its id when you need it fresh. zatcaStatus=pending on GET /v1/sales-invoices and GET /v1/credit-notes lists the documents still waiting for ZATCA. ZATCA results that arrive later are pushed to you by webhook; see Webhooks.
Write without duplicates
Two things protect a write, and they work together:
Idempotency-Key: sending the same request again with the same key returns the first reply and creates nothing. Kosoor keeps a key for 30 days. See Retries and idempotency.referenceId: your own id for the record, such as your ERP's document number. No two records of the same kind may share one:
| Resource | referenceId is unique across |
|---|---|
| customers | the merchant's customers; it is set once and never changes |
| sales invoices | the merchant's sales invoices and credit notes together |
| payments | the merchant's payments |
| credit notes | the merchant's sales invoices and credit notes together |
So if your system crashes after sending an invoice and loses its Idempotency-Key, sending the invoice again under a new key cannot create a second one: it answers 409 reference_id_taken. existingId names the record that holds the reference when it is the same kind of record and your key may read it, and GET /v1/sales-invoices?referenceId=... finds it in any case:
{
"error": {
"type": "invalid_request_error",
"code": "reference_id_taken",
"message": "Another resource already has this referenceId.",
"param": "referenceId",
"docUrl": "https://docs.kosoor.com.sa/errors#reference_id_taken",
"existingId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"
}
}
Before you write, you can also look the reference up: every list takes a referenceId filter.
Map the catalogue once
Your ERP's branches, items and payment methods map to Kosoor ids from GET /v1/branches, GET /v1/items and GET /v1/payment-methods. The sandbox keeps the live ids of the catalogue it copied, so a mapping you build in the sandbox holds on live data. Customers are the exception: the sandbox has its own, so map them again on live data, by referenceId, phone or VAT number.