Idempotency
Retry safely with the Idempotency-Key header — keys reserved before the run, replays of successful calls for 24 hours, the Idempotent-Replayed header and IDEMPOTENCY_CONFLICT.
A timeout or a dropped connection can lose the response of a decision that did run. Retrying blindly would run — and bill — it twice. Send an Idempotency-Key and a retry gets the stored response instead.
Idempotency-Key: lead-8421The key is 1 to 128 characters from A–Z a–z 0–9 . _ : -. Anything else fails with 400 INVALID_REQUEST.
Behavior
| Situation | What happens |
|---|---|
| New key | The key is reserved for this request before the decision runs. If the decision succeeds (200), its response is stored for 24 hours. |
| Same key, same request, within 24 hours | The stored response is returned with the header Idempotent-Replayed: true — same execution_id, same answers, same destination entries. The decision doesn't run again, nothing is billed, nothing is delivered and no execution is recorded. |
| Same key, same request, while the first one is still running | 409 IDEMPOTENCY_CONFLICT with Retry-After: 1 — "A request with this Idempotency-Key is still running. Retry in a moment to get its response." Retry after a second: once the first request finishes, you get its stored response. The decision never runs twice. |
| Same key, different request, within 24 hours | 409 IDEMPOTENCY_CONFLICT without Retry-After — "This Idempotency-Key was already used with a different request body." — even while the first request is running. |
| Same key after 24 hours | Treated as a new key. |
| The first request failed | The reservation is released, so a retry with the same key runs the decision again. |
The first request got the engine-error fallback (action_reason.type = "engine_error") | The reservation is released too: a retry with the same key reaches the engine again — and its destinations fire again, with new delivery IDs. |
| The first request never finished — the server stopped mid-run | The reservation expires after 2 minutes; then the key can run again. |
"Same request" means the same slug, the same state and the same include query value. The state is compared after JSON parsing: whitespace doesn't matter, but the order of keys does — {"a":1,"b":2} and {"b":2,"a":1} are different requests. Build the body the same way on every attempt.
Choosing keys
Use one key per logical operation and reuse it across the retries of that operation:
- the ID of the thing you're deciding about:
lead-8421,ticket:9f2c1d,msg_01HZX3…; - or a UUID generated once when the operation starts — not one per attempt.
Keys are scoped to the API key that sent them: the same Idempotency-Key used with a live and a test key are two different keys.
Example
# First call: runs the decision.
curl -i -X POST https://api.dcision.io/v1/decisions/spam-detection \
-H "Authorization: Bearer $DCISION_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg-20261005-0042" \
-d '{ "state": "Congratulations! You won a $1000 gift card, click here to claim now." }'
# Same call again: replayed.
# HTTP/1.1 200 OK
# Idempotent-Replayed: trueSending the same call again while the first one is still running returns 409 with Retry-After: 1; retry after a second to get its response:
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "A request with this Idempotency-Key is still running. Retry in a moment to get its response.",
"request_id": "req_Hn5Wq2Lk8Tz1Vb4Xc7Pm"
}
}Sending a different state with msg-20261005-0042 within 24 hours returns:
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "This Idempotency-Key was already used with a different request body.",
"request_id": "req_Fw2Lk9Tq7Mz1Vb6Xc3Pn"
}
}Good to know
- Replays are served first. A replay is returned even if the decision has since been redeployed or disabled: you get exactly what the first call returned, including its
version. - Replays count toward the rate limit, like any authenticated request.
- Concurrent duplicates are safe. The key is reserved before the decision runs, so two requests sent at the same time with the same key never both run — or bill, or deliver: the second one gets
409withRetry-After: 1, and its retry gets the stored response. The SDKs wait and retry on their own. - Errors free the key. The key is checked right after authentication, the rate limit and the body: any later failure — an unknown or disabled decision, the quota, an invalid state, an engine error — releases it, and the next attempt starts over.
- Stored responses contain the answers. They are kept for 24 hours whatever the decision's
storeOutputsetting; the state itself isn't stored for idempotency. - The SDKs and the CLI do it for you. The SDKs send an
Idempotency-Keywith everydecidecall and reuse it on their retries;dcision decidegenerates one when you don't pass one and retries once on network errors — see CLI.
See Idempotent retries for a complete client in JavaScript and Python.
Rate limits
Per-workspace request limits for each plan, separate windows for decisions and reads, the X-RateLimit headers on every call, Retry-After on 429 and how to back off.
Limits
Every size and count limit in one place — request body, state and token budget, decision schema, destinations and deliveries, identifiers, workspace, team and sign-in limits.