Skip to main content

Idempotency Headers

Adjudon's mutating endpoints accept an Idempotency-Key header so a client can retry a failed network call without producing duplicates. The mechanism is per-organisation, persistent across restarts, and fails open when the store is unavailable. Tested against API version v1.

The header

HeaderRequiredFormatNotes
Idempotency-Keyoptionalstring, ≤256 characters, case-insensitiveIf absent, Adjudon auto-generates a deterministic key from the request hash (see below)
Idempotency-Key: 9f0e2c4b-1a3d-4f5e-8b0c-6e1d2a3b4c5d

A v4 UUID per request is the canonical client choice. Adjudon does not require the value to be a UUID; any opaque string up to 256 characters works.

TTL: 24 hours

Idempotency records are stored for 24 hours after the first request. A MongoDB TTL index purges them automatically when the window closes. After 24 hours, the same key on the same payload is treated as a fresh request, not a duplicate.

This matches the standard window for replay-safe client retries. Long-running batch jobs that retry after more than 24 hours should generate fresh keys per attempt.

Auto-generated keys

When the client does not send the header, Adjudon computes a key itself:

key = sha256(`${agentId}:${organizationId}:${JSON.stringify(body)}`)

Two identical payloads from the same agent in the same organisation produce the same key — a second POST with byte-identical body returns the cached response, not a new trace. This is by design and is the same behaviour as a client-supplied duplicate key. If you want explicit retry control, send your own Idempotency-Key.

The auto-generation is deterministic across server instances (the hash uses no clock or randomness), so a retry that lands on a different API node behaves identically to one that lands on the original.

Behaviour matrix

ScenarioServer response
First request with new keyProcess normally, store the response keyed by (orgId, key)
Second request with same key, original completedReturn the cached statusCode + responseBody; no new resource created
Second request with same key, original still processingTreated as a concurrent duplicate; the in-flight request wins the slot
Second request with same key, payload differsSame cached response is returned (the key, not the body, is the dedup unit)
Same key from a different organisationIndependent; chains never mix across tenants
Idempotency store unavailableFail open — the request proceeds without idempotency protection; never a blocker

The fourth row is the gotcha: the key is the dedup unit, not the body. If you reuse a key with a different body, you receive the original response, not a new one. Generate a fresh key per logical operation.

Worked example

curl — first request
curl -X POST https://api.adjudon.com/api/v1/traces \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Idempotency-Key: 9f0e2c4b-1a3d-4f5e-8b0c-6e1d2a3b4c5d" \
-H "Content-Type: application/json" \
-d '{"agentId":"a1","inputContext":{"prompt":"x"},"outputDecision":{"action":"deny"}}'
# → 201 Created, traceId: trace_aBcD1234
curl — retry with same key
# Network hiccup, client retries with the same key
curl -X POST https://api.adjudon.com/api/v1/traces \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Idempotency-Key: 9f0e2c4b-1a3d-4f5e-8b0c-6e1d2a3b4c5d" \
-H "Content-Type: application/json" \
-d '{"agentId":"a1","inputContext":{"prompt":"x"},"outputDecision":{"action":"deny"}}'
# → 201 Created, traceId: trace_aBcD1234 (same trace, NOT a duplicate)
Python — retry-safe POST helper
import os, uuid, requests
def post_trace(payload, key=None):
return requests.post(
"https://api.adjudon.com/api/v1/traces",
headers={
"Authorization": f"Bearer {os.environ['ADJUDON_API_KEY']}",
"Idempotency-Key": key or str(uuid.uuid4()),
},
json=payload,
)

Endpoints that accept the header

The header is honoured on every mutating endpoint. The trace-ingestion endpoint (POST /api/v1/traces) is the canonical case. Other mutating endpoints (e.g., POST /api/v1/incidents, PATCH /api/v1/policies/:id) accept the same header with the same 24-hour TTL.

GET and HEAD requests are read-only and do not need idempotency.

See also