How it works
Send anIdempotency-Key header on POST /v1/invoices (and any future write that documents the header):
- The key is any string of 1–255 printable ASCII characters, scoped to your environment. Derive it from something unique on your side (your order id, your job id), or use a UUID.
- The first request with a key executes normally and its response is stored for 24 hours.
- A retry with the same key and the same body returns the stored response without executing again, marked with an
Idempotent-Replayed: trueheader. The invoice is issued exactly once no matter how many times you retry. - The same key with a different body is rejected with
409 idempotency_conflict: a key names one logical request, not a slot to reuse. - While the original request is still running, a concurrent retry answers
409 idempotency_conflict. Wait and retry: once the original completes you get its stored response.
Failures
A failed request ends in one of four states. Which one decides whether the key can be used again. The key is never touched. Three failures happen before any record is stored: a missing or invalid API key (401), a malformed or schema-invalid body (400 invalid_request), and an Idempotency-Key header that is not 1–255 printable ASCII characters (also 400 invalid_request). The key stays completely unused. For the first two, fix the request and send it again with the same key. For the third, the key itself is the problem: send the request again with a valid key.
The answer is stored and replayed. This covers deterministic rejections the invoice logic produced, such as invalid_tax_id. Retrying the same request repeats the same answer without re-executing. Fix the request and send it with a fresh key, because the old key now names the old body.
The key is released. Nothing was persisted, so a retry with the same key re-executes against the state at that moment. What you wait for differs by code:
- Any
503. Wait for the interval in theRetry-Afterheader, then retry. Three codes reach it today.tax_id_validation_unavailablemeans the census is down, so retry once it recovers.recipient_changed_during_validationmeans another operation changed the corrected invoice’s recipient tax data mid-request, so the retry re-validates against the current one.invoice_pending_at_aeatmeans the invoice being corrected is still registering at the tax agency, so the retry runs once it has. - The two account gates,
no_active_subscription(402) andaeat_mandate_not_approved(403). Both are checked before anything is written. Neither is cleared by time, so neither carries aRetry-Afterand neither should be retried on a timer.no_active_subscriptionclears when the company subscribes.aeat_mandate_not_approvedclears when the company finishes onboarding and Finseed approves its AEAT representation documentation, which is a human review and can take days. Retry once the company confirms it can issue live invoices. The key is released so that retry works with the key you already used.
409 for up to 24 hours rather than risking a duplicate. If you never received a response and the 409 persists: with an external_id, retry with a fresh key. If the original landed, the retry is rejected with 409 external_id_already_used and names the existing invoice. If it did not land, the retry issues it. No lookup step is needed. Without an external_id, do not retry with a fresh key. To learn whether the original landed, page through GET /v1/invoices and look for an invoice matching what you sent (customer, total, and a created_at close to the time of the request), or contact support.
The window is 24 hours: also send an external_id
The stored response is kept for 24 hours. That covers what the key exists for: a transport failure retried within minutes or hours. It does not cover a caller that keeps re-sending the same job for days. On day two the key has expired, the request runs as new, and a second invoice is issued.
For that case send an external_id on the invoice, typically your order id. It is unique per environment for as long as the account exists: a later request carrying the same value is rejected with 409 external_id_already_used, whatever the age of the key and whatever the body, and the error names the existing invoice. See Deduplicate with an external id.
What not to do
- Do not reuse a key across different invoices, even after the previous one succeeded.
- Do not retry a 409 in a tight loop; use backoff and let in-flight requests finish.
- Do not skip the header on invoice creation. It is optional in the schema, mandatory in spirit.