Idempotency
Issuing a document consumes a sequential number and, for a tax invoice, an allocation number from the Tax Authority. A duplicate cannot be deleted — only credited. That is the difference between a timeout being a retry and a timeout being an accounting correction, and it is what the two mechanisms on this page exist to prevent.
Idempotency-Key
Send an Idempotency-Key header on POST /documents/create/{type} and repeat the same key on every retry of that call. A key is any string you choose, up to 255 characters — a UUID per business operation is the usual choice. Keys are scoped to your company.
POST /documents/create/invoice
Authorization: Bearer glnc_...
Idempotency-Key: 8f1d2c40-6e3a-4b91-92f0-7c5a1d8e4b06
Content-Type: application/json
{ "clientId": 42, "products": [ ... ] }| Situation | Response |
|---|---|
| Same key, same request body | 200 with the original response, plus Idempotent-Replay: true. Nothing new is issued. |
| Same key, different body | 409 IDEMPOTENCY_KEY_REUSED — use a new key for a new document. |
| Same key, first call still running | 409 REQUEST_IN_PROGRESS — wait a moment and retry. |
| Malformed key | 400 INVALID_IDEMPOTENCY_KEY |
| First call was rejected (4xx / 5xx) | The key is released. Fix the body and retry with the same key. |
/documents/preview and /documents/preview/html issue nothing and consume no counter, so the header is ignored there — a preview is regenerated every time on purpose.externalId — your own identifier
externalId is a body field on document creation: your own id for the document, up to 64 characters, unique within your company. Where Idempotency-Key protects a single call, externalId protects the operation — it still works from a worker that has lost the original key, or from a different process entirely.
Creating with an externalId that already exists returns the document issued under it, with alreadyExists: true — a 200, not an error, because the state you asked for is the state that exists. Check the flag when you need to tell a replay from a fresh issue.
{
"id": 84213,
"visibleId": "d0f3a1c7-5b2e-4d8a-9f6c-1e3b5d7a9c2f",
"type": "INVOICE",
"number": 1042,
"externalId": "erp-9912",
"alreadyExists": true
}And when a create times out with no response at all, look the document up by the id you sent:
GET /documents/by-external-id/erp-9912
It returns exactly the GET /documents/id/:visibleId payload, or 404 if nothing was issued — which is your signal that retrying is safe.
Recommended Pattern
// One id per business operation, stored before the call.
const externalId = `erp-${order.id}`;
const idempotencyKey = crypto.randomUUID();
try {
const doc = await createInvoice({ externalId, idempotencyKey, ...payload });
if (doc.alreadyExists) markAlreadyInvoiced(order, doc);
else markInvoiced(order, doc);
} catch (err) {
if (err.timeout) {
// Never re-POST blindly — ask first.
const existing = await getByExternalId(externalId); // 404 => safe to retry
if (existing) markInvoiced(order, existing);
}
throw err;
}