Idempotency
The API supports idempotency so requests can be safely retried without executing the same operation twice. Useful when a call is interrupted in transit and you don't get a response — for example, a request to create a subscription that drops on a network error can be retried with the same key, guaranteeing that only one subscription is created.
How to use it
Send the X-Idempotency-Key: <key> header on any POST, PUT or PATCH request. The key is opaque and generated on your side. We recommend UUID v4 or any random string with enough entropy to avoid collisions.
curl -i \
-H "Authorization: Bearer $KOBANA_TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-Idempotency-Key: 8c0f5d6e-3f8b-4cb5-9a47-d8f5b15e9b21' \
-H 'User-Agent: Kevin Mitnick <kmitnick@kobana.com.br>' \
-d '{
"subscription": {
"billing_account_id": "ba_01HXY123",
"plan_id": "plan_01HPRO",
"billing_cycle": "monthly"
}
}' \
-X POST 'https://api.billing.sandbox.kobana.com.br/v1/subscriptions'
How it works
When the server receives a request carrying X-Idempotency-Key:
- Reserves the key atomically, scoped to your organization. Two different organizations can use the same key without conflict.
- Computes a request fingerprint — SHA-256 of
method + path + raw body. The hash is stored alongside the key. - Runs the handler. If it returns a response, the server records
status_code, body, and relevant headers tied to the key. - Marks the key as completed. Any subsequent request reusing the key within 24 hours receives the original response verbatim — same
status_code, same body, same headers — plus anX-Idempotent-Replay: trueheader that flags it as a replay.
The layer records the result regardless of success or failure. Retries with the same key return the same result, including 5xx errors.
Limits
- Maximum key length: 255 characters.
- Retention: keys expire automatically 24 hours after creation. After expiry, a new request reusing the key yields a fresh result.
- Scope: per organization. A key used in one organization does not interfere with another.
- Accepted characters: any printable UTF-8 string.
When the result is not recorded
The layer only records the result once the handler started executing. Nothing is stored in the following cases — it is safe to resend the request:
- The key is empty or longer than 255 characters →
400 bad_request. - The API envelope rejects the request before the handler (invalid JSON, missing authentication, etc.).
- The handler throws an exception mid-execution — the reservation is released so the next retry can run cleanly.
- Another request with the same key is currently executing →
409 conflictwithdetails.reason = "idempotency_request_in_progress".
Conflicts
If the same key is reused with a different request (any change in method, path, or body), the API responds 409 conflict with details.reason = "idempotency_key_reused". This guards against accidental reuse — for example, persisting the key alongside the operation on your side and then trying to create two different resources under the same identifier.
Error response
{
"error": {
"code": "conflict",
"message": "X-Idempotency-Key was already used with different request parameters. Reuse the original parameters or generate a new key.",
"details": { "reason": "idempotency_key_reused" }
}
}
Other possible responses
| Scenario | Status | error.code | details.reason |
|---|---|---|---|
| Another request with the same key is running | 409 | conflict | idempotency_request_in_progress |
| Same key reused with a different payload | 409 | conflict | idempotency_key_reused |
| Key empty or longer than 255 characters | 400 | bad_request | invalid_idempotency_key |
Accepted methods
| Method | Accepts X-Idempotency-Key? |
|---|---|
POST | ✅ Yes |
PUT | ✅ Yes |
PATCH | ✅ Yes |
GET | ❌ No (already idempotent by definition) |
DELETE | ❌ No (already idempotent by definition) |
Sending the key on GET or DELETE has no effect — the API processes the request normally without recording anything.
Detecting a replay
Every response served from the recorded result carries the header:
X-Idempotent-Replay: true
You can use this header on your side to tell the original execution apart from a server-side replay.
Best practices
- One key per logical operation. Don't reuse the same key to create two different resources — you will get
409 conflict. - Persist the key alongside the operation state on your side, so a retry after a crash or restart still uses the same key.
- Pair it with exponential backoff on
5xxor timeout responses — the key guarantees the retry doesn't duplicate the resource. Because failures are also recorded, you can stop retrying as soon as the expected status code comes back. - Drop the key after 24 hours. No value in keeping it on your side beyond that — the server has already discarded the record.
- Don't send the key on methods that don't accept it. It is silently ignored on
GET/DELETE— we prefer clients to omit it so the contract is explicit about what's idempotent and what relies on the key.