Skip to content
Live in production, in shadow mode: FingerTrackID recommends, your system decides.Status →

Idempotency and retry

There are two levels of identity, and they do not mix:

  • Logical operation: idempotency_key + external_event_id, in the body. They stay the same on every retry and are never reused for another operation, not even after the data expires.
  • Send attempt: X-FTID-Request-Id, X-FTID-Timestamp and the signature. They change on every attempt.
Situation Response
New key 201, with the newly stored assessment
Same key, same content, result still within its approved history window 200, with exactly the assessment already stored, without recomputing — even if the original occurred_at is already outside the ±300 s window
Same key, same content, result outside its approved history window 410 assessment-result-expired, with no historical result or assessment identifier; no recomputation or new usage
Same idempotency_key or same external_event_id with different content 409 idempotency-conflict, with no partial effect
Person, account or session references incompatible with bindings already stored, for example an account_ref first sent with another subject_ref 409 binding-conflict, with nothing stored; not retryable

“Content” means the ten body fields other than deadline_ms. Changing only deadline_ms keeps the same operation. Changing any other field creates a conflict — including replacing null with a token, changing the token or rewriting occurred_at with a different spelling (Z and +00:00 are different spellings).

The event_context_token is single-use and is consumed together with the assessment. A retry of the same operation does not consume it again; a new operation with an already used token receives 400 event-context-invalid.

409 · same key, different content
{
"type": "https://fingertrackid.dev/problems/idempotency-conflict",
"title": "Chave de idempotência já usada com outro conteúdo",
"status": 409,
"correlation_id": "corr-problem-idempotency-0001",
"retryable": false
}
Result Retry? How
201 or 200 No Use the assessment
401 timestamp-out-of-window Yes Fix the clock; new attempt
401 request-replayed Yes New attempt — and fix the code that reused the request_id
429 rate-limited Yes Wait for Retry-After (today 1 s; at least 1 s if it is missing); new attempt
503 assessment-unavailable Yes Wait for Retry-After (today 1 s; at least 1 s if it is missing); new attempt. The state may be indeterminate
410 assessment-result-expired No The old result is no longer available. Stop reconciliation retries; preserve the original operation IDs and do not regenerate the operation
Network error, timeout, truncated response or response above 65536 bytes Yes Treat as indeterminate; new attempt with the same body
Any other 4xx, or 500 No Fix the cause (see errors)

A new attempt is always: same body, new request_id, current timestamp and new signature. This applies to 429 and 503 too: the request_id of the answered attempt may already have been consumed. Retry a bounded number of times, never in a loop.

One operation, two attempts
Attempt 1 same body bytes
X-FTID-Request-Id: <new UUIDv7>
X-FTID-Timestamp: <now>
X-FTID-Signature: <new>
→ 503 assessment-unavailable, Retry-After: 1
wait at least Retry-After
Attempt 2 the same body bytes
X-FTID-Request-Id: <another new UUIDv7>
X-FTID-Timestamp: <now>
X-FTID-Signature: <new>
→ 200 with the assessment already stored,
or 201 if the first attempt stored nothing

After a timeout, the assessment may have been stored without the response reaching you. A retry with the same body resolves it: a retained result returns 200; an expired result returns non-retryable 410. Use a bounded retry policy — backoff, Retry-After and the deadline of your business operation. Once that deadline is exhausted, apply your own fallback and reconcile later, with the same body.

A body that carries an event_context_token reconciles this way only if the first attempt was stored, or while the token is still before its expiresAt (at most your application’s evidence window, 60 or 300 seconds, after its collection). Otherwise the same body returns 400 event-context-invalid; assess the event as a new operation with null instead (integration guidance).

There is no recovery GET: recovery is the same POST.