Skip to main content
A network timeout leaves you not knowing whether your request landed. For most APIs that is an inconvenience; for an invoicing API it is a legal problem, because a duplicated invoice can only be undone with a corrective invoice. Idempotency keys remove the dilemma: retrying is always safe.

How it works

Send an Idempotency-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: true header. 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 the Retry-After header, then retry. Three codes reach it today. tax_id_validation_unavailable means the census is down, so retry once it recovers. recipient_changed_during_validation means another operation changed the corrected invoice’s recipient tax data mid-request, so the retry re-validates against the current one. invoice_pending_at_aeat means 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) and aeat_mandate_not_approved (403). Both are checked before anything is written. Neither is cleared by time, so neither carries a Retry-After and neither should be retried on a timer. no_active_subscription clears when the company subscribes. aeat_mandate_not_approved clears 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.
The key stays blocked. This is a request that ended in a truly unknown state, such as a crash mid-flight. The key answers 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.