Idempotency and Dedupe
Prevent duplicate requests while keeping repeated occurrences visible to reviewers.
ActionBox has three related protections with different jobs:
| Mechanism | Protects | What repeats do |
|---|---|---|
Idempotency-Key header | Transport retries of a create or outcome request | Returns the original result for the same logical request |
dedupe_key body field | Repeated occurrences of the same open workflow | Updates one open Action and increments occurrence_count |
action_version + fingerprint | Decisions made from a reviewed snapshot | Rejects a stale decision with 409 ACTION_CHANGED |
Do not use one as a substitute for another.
Protect Action creation
Send a stable, unique key for the logical operation:
curl -fsS -X POST https://api.actionbox.cloud/v1/actions \
-H "Authorization: Bearer $ACTIONBOX_SOURCE_KEY" \
-H "Idempotency-Key: deploy-build-42" \
-H "Content-Type: application/json" \
-d '{
"title": "Approve the production deploy",
"dedupe_key": "production-deploy-build-42",
"options": [
{"id": "approve", "label": "Approve"},
{"id": "reject", "label": "Reject"}
]
}'The key is scoped to the authenticated Source and its live/test environment. ActionBox retains it for 24 hours. During that window:
- the same key with the same logical request returns the original Action and
idempotent_replay: true; - the same key with changed request content returns
409 IDEMPOTENCY_KEY_REUSED; - a key longer than 255 characters returns
INVALID_IDEMPOTENCY_KEY; - an empty key is treated as no idempotency key and does not deduplicate a create request.
The key is not global: another Source, or the other environment of the same Source, has its own boundary. Never derive a key from a secret, and never reuse one for an unrelated operation.
Persist the key before sending
Generate or choose the key before the first network attempt and persist it with the worker's job/run record. If the response is lost, retry the exact request with that same key instead of creating a new key.
Merge repeated open occurrences
dedupe_key is scoped to the Source and environment. When a request arrives
with a matching open Action, ActionBox updates that Action, increments its
occurrence_count, and records an update event. It does not queue a terminal
webhook merely because the occurrence was deduped.
If the matching Action is already resolved, cancelled, or expired, the
next request creates a new Action. Dedupe therefore keeps an active incident
visible without hiding a later occurrence after the first one is complete.
Use a stable workflow identity for dedupe_key, such as a deployment target
and release line. Include a run or attempt identity in Idempotency-Key when
each attempt must be retried independently.
Bind a human decision to what was reviewed
Every Action response includes action_version and a canonical SHA-256
fingerprint. A material update—including a dedupe update—advances the version
and changes the fingerprint. Send both values from the snapshot that the human
reviewed:
{
"action_version": 2,
"fingerprint": "sha256:<64 hex characters>",
"option_id": "approve"
}If the Action changed after it was displayed, the API returns 409 ACTION_CHANGED
with the current binding. Re-read the Action and require the
caller to review the new content. This is intentionally fail-closed.
SDK behavior
The SDKs generate an idempotency key automatically when creating an Action, or accept one explicitly when a job needs a stable key:
action = client.create(
title="Approve the production deploy",
options=[{"id": "approve", "label": "Approve"}],
idempotency_key=f"deploy-{build_id}",
)const action = await client.create(
{ title: "Approve the production deploy", options: [{ id: "approve", label: "Approve" }] },
`deploy-${buildId}`,
);Outcome helpers use a deterministic outcome-{action_id} key so an exact
transport retry is safe. A different second outcome is rejected because the
execution record is immutable.
Next: Errors and safe retries, Decisions and outcomes, or Testing with sandbox keys.