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-8421

The key is 1 to 128 characters from A–Z a–z 0–9 . _ : -. Anything else fails with 400 INVALID_REQUEST.

Behavior

SituationWhat happens
New keyThe 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 hoursThe 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 running409 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 hours409 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 hoursTreated as a new key.
The first request failedThe 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-runThe 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: true

Sending 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:

409 Conflict — still running
{
  "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:

409 Conflict
{
  "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 409 with Retry-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 storeOutput setting; the state itself isn't stored for idempotency.
  • The SDKs and the CLI do it for you. The SDKs send an Idempotency-Key with every decide call and reuse it on their retries; dcision decide generates 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.

On this page