Idempotency
A write that times out leaves you not knowing whether it happened. Send an idempotency-key with it, and retrying with the same key and the same body is guaranteed to apply at most once: a retry after the first request succeeded gets that first response back, replayed, instead of doing the work again.
bash
curl -sS https://<your-app-domain>/api/public/v1/contacts/import \
-H "authorization: Bearer $CALL_CENTER_API_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: crm-sync-2026-10-09-chunk-0042" \
-d @chunk-0042.jsonEvery write endpoint (POST, PUT, PATCH, DELETE) honours it; reads ignore it. It is optional, but you should send one on anything you might retry.
Choosing a key
- 1–255 printable ASCII characters. Anything else is a
400. - One key per operation, not per attempt: generate it before the first try, and send the same one on every retry of that operation. A UUID works; so does something you can reconstruct, like a sync run and chunk number.
- Keys are scoped to the API key that sent them, so two integrations cannot collide even if they choose the same string.
What happens on a retry
| The first request… | A retry with the same key gets |
|---|---|
succeeded (2xx) | The same status and body, with idempotent-replayed: true. |
| is still running | 409 CONFLICT. Wait a moment and retry with the same key. |
was refused (4xx) or failed (5xx) | A fresh attempt. A refusal wrote nothing, so the key is released. |
| had a different body | 400 INVALID_INPUT. A key names one request; use a new key for a new one. |
A successful response is kept for 24 hours (API.IDEMPOTENCY.TTL_SEC in src/lib/const/api.const.ts). After that the key is forgotten and a request with it runs as new.
Because a refusal releases its key, you can fix what was refused and retry under the same key: top up credit after a 402, wait out a 429, or correct a field after a 400 — the corrected body is then that key's request.
A replayed response is a real response in every other way: it carries this request's x-request-id, current rate-limit headers, and it costs the endpoint's rate-limit tokens like the original did.
"Same body" means byte-for-byte. Re-serialising the same object with keys in a different order is a different body — retry with the bytes you sent the first time.
Endpoints that are already safe to repeat
Some writes are idempotent on their own. Contacts import is an upsert, so re-posting a chunk without a key is still safe — but its counts describe what that request did, so a re-post reports rows the first attempt created as updated. With a key, a retry gets the first attempt's exact counts back.