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": [ ... ] }
SituationResponse
Same key, same request body200 with the original response, plus Idempotent-Replay: true. Nothing new is issued.
Same key, different body409 IDEMPOTENCY_KEY_REUSED — use a new key for a new document.
Same key, first call still running409 REQUEST_IN_PROGRESS — wait a moment and retry.
Malformed key400 INVALID_IDEMPOTENCY_KEY
First call was rejected (4xx / 5xx)The key is released. Fix the body and retry with the same key.
Previews are exempt. /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.

// 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;
}
Use both. The header makes the HTTP call safe to repeat; the field makes the operation safe to repeat. Together they mean no sequence of timeouts, restarts or retries can issue the same invoice twice.

Last updated