Using the APIIdempotency

Using the API

Idempotency

Send an Idempotency-Key with every write so a retried request never creates, sends or charges anything twice.

Networks fail. A request can time out after the server finished the work but before you saw the answer. If you simply retry, you might create a second invoice, record a payment twice or email a client twice. The Idempotency-Key header prevents that: the same key with the same request always gives the same result, and the work happens only once.

How to use it

Add an Idempotency-Key header to every POST, PUT, PATCH and DELETE. The value is any string you choose, up to 255 characters, that is unique for the operation you mean.

cURL
curl -X POST "https://api.freeinvoiceautomation.com/v1/payments" \
  -H "Authorization: Bearer $FIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bank-2027-01-14-ref-88231" \
  -d '{
    "document_id": "0193f2b4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
    "amount_minor": "70000",
    "currency": "USD",
    "received_at": "2027-01-14T00:00:00Z",
    "reference": "88231"
  }'

If the call times out, send exactly the same request with the same key again. You get the original result back.

When it is required

For calls made with an API key or an MCP token, the key is required (proposed) on:

It is optional, and still recommended, on every other write.

What happens on a repeat

You sendWe answer
A new keyThe request runs normally and its response is stored
The same key and the same body againThe stored response, with the header Idempotent-Replayed: true. Nothing runs a second time.
The same key with a different body409 idempotency_key_reused. Nothing runs.
The same key while the first request is still running409 idempotency_in_progress. Wait a moment and retry.

If you change the request, for example to fix a field, it is a different operation: use a new key.

Replayed response headers
HTTP/1.1 201 Created
Idempotent-Replayed: true
X-Request-Id: req_0193f2a2c0e1

How long keys are kept

Keys are stored per account and per caller (the API key, user or token that sent them) for a set time, proposed at 24 hours. After that, the same key is treated as new. Retries normally happen within seconds or minutes, so this window is more than enough; do not rely on a key to block a duplicate days later.

Because keys are scoped to the caller, two different API keys can use the same key string without affecting each other.

Choosing good keys

A good key identifies the thing you mean to do, not the attempt.

  • Derive it from your own data when you can: your order id, your payment reference, your job id. Then even a restarted process sends the same key for the same work.
  • Otherwise generate a random UUID when you decide to do the operation, store it with your pending work, and reuse it on every retry.
  • Do not generate a new key per attempt. A fresh key on each retry gives no protection.

Our own connectors use deterministic keys built from the store event, for example shopify:{shop}:{order_id}:orders/create (proposed) and woo:{site}:{order_id}:processing. You can follow the same pattern:

billing-server:invoice:customer-4821:2027-01
billing-server:payment:stripe-export:py_3Px...

Sends are protected twice

Sending a document is also protected on our side regardless of the header: each send is unique per document, event and channel. A retried send never emails the same document twice for the same event, even if the retry used a different key. This also covers reminders.

Webhooks and the other direction

Idempotency also matters when we call you. Outgoing webhooks keep the same event id on every retry, and your endpoint should ignore an id it has already processed. Inbound payment events from Stripe and the stores are deduplicated per connection and event on our side, so a provider that retries never marks an invoice paid twice.

Retry strategy

1

Pick the key before the first attempt

Build it from your own ids or generate a UUID, and save it with the pending task.

2

Retry on network errors and 5xx

On a timeout, connection reset, 500 internal_error, 502 upstream_error or 503, wait and retry with the same key and body. Use exponential backoff.

3

Wait on 409 idempotency_in_progress and 429

The first attempt is still running, or you are over a rate limit. Wait (for 429, wait as long as Retry-After says) and retry.

4

Do not retry 4xx validation or permission errors unchanged

422, 403 and 404 will fail the same way again. Fix the request and use a new key.

Errors

CodeStatusFix
idempotency_key_reused409You reused a key for a different request. Use a new key for each distinct operation.
idempotency_in_progress409The first request with this key has not finished. Retry after a short wait.