Docs menu

Idempotency

A network error after you send a write leaves you unsure whether it happened. An Idempotency-Key makes retrying safe.

How it works

Send a unique Idempotency-Key header with any POST, PATCH or DELETE:

curl https://api.officehero.io/v1/invoices \
  -H "Authorization: Bearer $OFFICEHERO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d0d6f9e-2f4a-4b4e-9a53-3c1b2f6a9e10" \
  -d '{"customer_id":"0199c3a1-7d4e-7b20-8a11-2f6c9d0e4a17","currency":"EUR","lines":[{"description":"Labour, 3 h","qty":3,"unit_price_minor":4500,"tax_rate":18}]}'

(Replace customer_id with one of your own Finance customers — take it from an existing invoice, GET /v1/invoices. The id above is an example and answers 422 invalid_reference — a refusal that is not stored, see below.)

For 24 hours, a second request from the same API key with the same key, method and path is not run again. You get the first response back — same status, same body — with one extra header:

Idempotency-Replayed: true

So the rule is simple: generate one key per logical operation (a UUID v4 is ideal), and reuse it for every retry of that operation.

What is stored

  • Responses with a status from 200 to 499 are stored and replayed. A 422 for a bad body (a missing or unknown member, a wrong type) replays as the same 422.
  • Two kinds of 422 are not stored: a reference to a record that does not exist (errors.common.validation.invalid_reference, e.g. an unknown customer_id) and a value the database cannot read as an id (errors.common.validation.invalid_input). A retry with the same key runs again, so you can correct the body and send it with the same key.
  • A 5xx is not stored: retry with the same key and the request runs again.
  • Responses larger than 256 KiB are not replayed: the retry gets a 409 (errors.platform.idempotency.replay_unavailable) telling you the first request already completed — read the resource instead.
  • Without the header, every request runs.

Mistakes it catches

Reusing a key with a different body is refused, because it is almost certainly a bug in the caller:

{
  "type": "https://officehero.io/errors/idempotency-conflict",
  "title": "Idempotency-Key reused with a different request body",
  "status": 409,
  "detail": "errors.idempotency.body_mismatch"
}

Bodies are compared as JSON, so the order of keys does not matter.

Scope

Keys are scoped to your company and your API key: two integrations — or two keys of one integration — can never see each other’s stored responses, even with the same Idempotency-Key.

Webhook deliveries also carry an Idempotency-Key header (equal to the event id) so your receiver can deduplicate them; see Retries.