A timeout does not tell you whether a write succeeded. On supported endpoints, send X-Idempotency-Key on the first attempt, then reuse it on retries. The reference lists the header on each endpoint that accepts it; sending it elsewhere has no effect.
Using a key
- Generate a unique key, such as a UUID, for one intended operation. Save it with the exact request and the user or service account making the call before sending it.
- Send that saved key in
X-Idempotency-Key. - Retry with the same key, request and authenticated actor. Refreshing an OAuth token for the same user is fine; switching to a different user or service account changes the actor.
Use a distinct key for a genuinely new operation. A hash of the body alone is not enough: two intended partial payments can have identical bodies, and posting a bill has no body at all. Do not generate a UUID inside your retry loop.
On bills, omitting the header or sending an empty or whitespace-only value disables deduplication. An invoice number is not a substitute for this header. Other resources can have different defaults; credit-note creation can derive a key from documentNumber.
Supported bill operations
All five headers are optional, but sending one lets you recover a lost response within the retention window.
| Operation | Route |
|---|---|
| Create a bill | POST /v1/invoice-payables |
| Post without approval | POST /v1/invoice-payables/{invoicePayableId}/post |
| Submit for approval | POST /v1/invoice-payables/{invoicePayableId}/submit-for-approval |
| Approve | POST /v1/invoice-payables/{invoicePayableId}/approve |
| Mark as paid | POST /v1/invoice-payables/{invoicePayableId}/mark-as-paid |
Separate line-item creation, header updates, document-upload URL creation, cancellation, decline and reverse-clearing do not accept this header. To create a bill and its lines in one protected call, use processingMode: "DATA_ONLY" with inline lineItems; see Create an invoice payable. The later upload is a separate step, not covered by the create key.
Key scope and request matching
Bill keys are scoped to company and action, not to an individual bill URL. Creating and posting a bill can use the same key because those are different actions. Posting two different bills with the same key in one company is a conflict, not two independent requests.
Light compares the bill ID, actor and translated request. Reusing a key for another bill, actor or different effective input returns 409 IDEMPOTENCY_VIOLATION. Matching is not a comparison of raw JSON bytes: ignored fields and normalised values may not change the translated input. Keep the original request unchanged rather than depending on that normalisation; for submission, also keep the same choice of an omitted body or a JSON body.
Responses and retries
| Retry with the same key | Result | What to do |
|---|---|---|
| Matching request, successful response cached | 200 with the original response; the operation is not repeated |
Keep the returned bill ID. Use GET for current state. |
| Matching request, key exists but no response is cached | 409 IDEMPOTENCY_IN_FLIGHT_REQUEST_IN_PROGRESS |
Retry with exponential backoff and the same key and request, subject to the retention limits below. |
| Different bill, actor or translated input | 409 IDEMPOTENCY_VIOLATION |
Check the saved operation. Do not blindly retry or replace the key after an uncertain outcome. Use a new key only for a genuinely new or deliberately corrected operation after reconciling the earlier attempt. |
These conflicts use the client-error envelope. Branch on errors[].type, not the message or HTTP 409 alone. Do not retry validation and permission failures blindly. For 429, follow Rate limits and retain the operation's key.
A replay is not a fresh read. A submission can still replay APPROVAL_REQUESTED after the bill has progressed to APPROVAL_PENDING or returned to IN_DRAFT with an asynchronous failure. Poll GET /v1/invoice-payables/{invoicePayableId}; replaying the submission does not restart the workflow. A deliberate resubmission after fixing the failure is a new operation and needs a new key.
Each genuine partial payment needs its own key, even if the amount and other fields match an earlier instalment. Retry a payment with its original key to avoid recording it twice.
Retention and uncertain outcomes
- A successful response is cached for 24 hours after completion. Replaying it does not extend the window.
- An in-flight key expires 60 seconds after the attempt starts. A failed bill attempt that acquired a key also leaves it until that expiry; the error response itself is not cached. A matching retry during that window gets the in-progress conflict; changed input gets a violation. Failures before the key is acquired, such as authentication failures, do not reserve it.
- Expiry is not proof that nothing happened. A slow operation may still be running after 60 seconds, and an operation may have succeeded without its response being cached. After expiry the same key can execute again. This is bounded retry protection, not a permanent exactly-once guarantee.
Use bounded retries. If the outcome is still unknown at expiry, reconcile it before another write: read the bill and, for payments, its payments list. When a create response was lost and you have no bill ID, reconcile against your saved source reference and the payables list. Do not automatically switch keys to bypass a conflict. A read-before-write check alone does not prevent concurrent duplicate creates.
Example
Set LIGHT_IDEMPOTENCY_KEY to a UUID you generated and saved for this create before running the command. Reuse that value and the same body if its response is lost; choose a different saved key for the next bill.
# Reuse this operation's saved key; do not generate a new one on each retry.
curl -X POST https://api.light.inc/v1/invoice-payables \
-H "Authorization: Basic $LIGHT_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: ${LIGHT_IDEMPOTENCY_KEY:?Set a saved key for this operation}" \
-d '{"amount":1500,"currency":"EUR","processingMode":"DATA_ONLY"}'
This creates a draft, not a bill ready for approval. The worked example adds the fields, lines and document needed for submission.