Skip to content

Idempotency and safe retries

Send a write twice without it happening twice.

Why it matters here

A connection can drop after your request arrived and before its response reached you. From your side the two look identical. Without a way to tell them apart, retrying risks a second invoice, or a second posted expense, and a posted entry is never deleted: undoing one means a reversing entry made by a person in the app.

So every POST on this surface accepts an Idempotency-Key header.

Send Idempotency-Key with a value that is unique to the thing you are doing. A uuid you generate once for that operation is the usual choice; your own order id works too. Generate it before the first attempt and send the same one on every retry of that request. It has to be printable ASCII, with no newline, and no longer than 200 characters. The header is optional: a POST without one runs normally. On a GET it is ignored rather than refused.

Request
curl -X POST "https://www.numm.io/api/v1/customers" \
  -H "Authorization: Bearer nmo_live_REPLACE_WITH_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3fee00" \
  -d '{
  "name": "Acme Co",
  "email": "ap@acme.example"
}'

The window

A stored result stays replayable for 24 hours. Repeat the same request with the same key inside that window and you get the first response back, with the same status and an idempotent-replayed: true header. The work does not run a second time.

After 24 hours the stored result is gone, and the same key runs the request again as if it were new. A key is a safety net for a retry, not a permanent lock on an action.

The two conflicts

  • The same key with a different request answers 409 conflict. A key is bound to the exact request it first ran, so reusing one for something else is refused rather than silently replayed.
  • A request with that key that has not finished, or finished without its result being saved, answers 409 conflict as well. Retry with the same key, never a new one. If this persists, check whether the write went through.

Retrying after a failure

A 4xx other than 409 conflict means the write did not run and the key is free: fix the request and send it again with the same key. On a conflict, never swap in a new key. Either that key is held, by a request with it that has not finished, or finished without its result being saved, or by one that already ran under it, or the write itself clashed, as a second reminder inside its cooldown does. message says which.

Once the work has run, the key is held. Retrying with it gives you back the stored response, or 409 conflict if the result could not be stored. A 500 can land on either side of that line, so treat it as held: send the same key again and read what comes back. Either way it never runs the write a second time.

That is the rule to lean on after a timeout, a dropped connection, or an internal_error: retry with the same key, not a new one. A new key is a new request, and a new request is a second write.

Keys belong to the API key that sent them

An Idempotency-Key is stored against the API key that sent it, not against your account. Two of your own keys can use the same value without colliding, and nothing you send on one key can replay a request made on another.

Every POST in the reference carries this header in its example request. Copy the shape from there.

Idempotency and safe retries — Nummio API