Skip to Content
APIIdempotency

Idempotency

A network can fail after your request arrived and before its response reached you. When that happens you cannot tell whether the operation took effect, and retrying blindly risks doing it twice — which for a registration means a person booked twice, and for an invoice means money.

Every write endpoint therefore requires an Idempotency-Key:

curl -sS -X POST https://nov3x.com/api/public/v1/registrations \ -H "Authorization: Bearer $NOVEX_API_KEY" \ -H "Idempotency-Key: 9f2c1a7e-4d5b-4a02-9c17-0f3f0a8d61e2" \ -H "Content-Type: application/json" \ -d '{"eventId":"evt_123","email":"sara@example.com"}'

Retry that exact request with that exact key and you get the original response back, byte for byte, carrying Idempotent-Replayed: true. The operation is not performed again.

The rules, in full

The header is required on writes. A write without it is refused with 400 IDEMPOTENCY_KEY_REQUIRED. We do not make it optional: “some of your retries are protected and some are not” is not a distinction anyone can hold in their head at 3 a.m.

A key is 1–255 printable ASCII characters, no whitespace. Anything else is 400 IDEMPOTENCY_KEY_INVALID.

Use a UUID per logical operation. Generate it before the first attempt, and reuse it for every retry of that attempt. Do not derive it from a timestamp, and do not reuse it for the next operation.

A key is not a nonce to be recycled. If you send the same key with a different body, we refuse with 409 IDEMPOTENCY_KEY_REUSED rather than guessing which request you meant. One key, one operation.

Keys last 24 hours — exactly. After that the key is free again and a request under it is a fresh operation, not a replay. Twenty-four hours is longer than any sane retry cycle, including one that spans an outage on your side. Note what that means: retrying with a day-old key does not return the old response, it performs the operation again — which is why a key is per attempt, and why the guidance below is to use a new one whenever you are unsure.

A replay still counts against your rate limit, and still appears in your workspace’s audit trail — marked as a replay, so three retries of one booking read as one booking.

Validation failures are remembered too. If your request was rejected with a 400, retrying the same key returns that same 400. It was a real answer to that exact request.

If a request did not complete, do not reuse its key

This is the rule that matters most, and it is the opposite of the usual advice.

If a write returns a 500, or your client times out, the outcome is unknown — we cannot tell whether your operation committed before things went wrong. So we hold the key rather than guess, and a retry with the same key returns:

{ "error": { "code": "IDEMPOTENCY_REQUEST_IN_PROGRESS", "message": "The original request under this key did not complete and we cannot tell whether it took effect. Check with a `GET` before retrying, and retry with a NEW `Idempotency-Key`." } }

That refusal is doing its job. What to do:

  1. GET the collection and look for the record.
  2. If it is not there, retry with a brand-new key.

A new key always proceeds, so you are never stuck — and the old one clears itself within 24 hours.

We could instead let a stalled key be taken over after some timeout. We deliberately do not: a timeout cannot tell “the original crashed before writing” from “the original wrote and then crashed”, so taking it over would gamble a double booking to save you one GET.

Errors you may see

StatusCodeMeaning
400IDEMPOTENCY_KEY_REQUIREDNo Idempotency-Key header on a write.
400IDEMPOTENCY_KEY_INVALIDThe key is empty, too long, or contains whitespace or non-printable characters.
409IDEMPOTENCY_KEY_REUSEDThe same key was used for a different request body or a different endpoint.
409IDEMPOTENCY_REQUEST_IN_PROGRESSThe original is still running, or did not complete. Reconcile, then use a new key.
409IDEMPOTENCY_RESPONSE_UNAVAILABLEThe original succeeded, but its response was too large to store. GET the record.
413REQUEST_TOO_LARGEThe request body exceeds what this API accepts.

See Errors for the full table, including why a 500 on a write is not the same as a 500 on a read.

Last updated on