Pular para o conteúdo
No ar em produção, em modo shadow: o FingerTrackID recomenda, o seu sistema decide.Status →

Idempotência e nova tentativa

Existem dois níveis de identidade, e eles não se misturam:

  • Operação lógica: idempotency_key + external_event_id, no corpo. Eles continuam os mesmos em toda nova tentativa e nunca são reutilizados para outra operação, nem mesmo depois que os dados expiram.
  • Tentativa de envio: X-FTID-Request-Id, X-FTID-Timestamp e a assinatura. Eles mudam a cada tentativa.
Situação Resposta
Chave nova 201, com a avaliação recém-armazenada
Mesma chave, mesmo conteúdo, resultado ainda dentro da própria janela de histórico aprovada 200, com exatamente a avaliação já armazenada, sem recalcular — mesmo que o occurred_at original já esteja fora da janela de ±300 s
Mesma chave, mesmo conteúdo, resultado fora da própria janela de histórico aprovada 410 assessment-result-expired, sem resultado histórico nem identificador de avaliação; sem recálculo nem novo uso
Mesmo idempotency_key ou mesmo external_event_id com conteúdo diferente 409 idempotency-conflict, sem efeito parcial
Referências de pessoa, conta ou sessão incompatíveis com vínculos já armazenados, por exemplo um account_ref enviado pela primeira vez com outro subject_ref 409 binding-conflict, sem nada armazenado; não admite nova tentativa

“Conteúdo” são os dez campos do corpo além de deadline_ms. Mudar apenas deadline_ms mantém a mesma operação. Mudar qualquer outro campo cria um conflito — inclusive substituir null por um token, trocar o token ou reescrever o occurred_at com outra grafia (Z e +00:00 são grafias diferentes).

O event_context_token é de uso único e é consumido junto com a avaliação. Uma nova tentativa da mesma operação não o consome de novo; uma nova operação com um token já usado recebe 400 event-context-invalid.

409 · mesma chave, conteúdo diferente
{
"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
}
Resultado Nova tentativa? Como
201 ou 200 Não Use a avaliação
401 timestamp-out-of-window Sim Corrija o relógio; nova tentativa
401 request-replayed Sim Nova tentativa — e corrija o código que reutilizou o request_id
429 rate-limited Sim Aguarde o Retry-After (hoje 1 s; pelo menos 1 s se ele faltar); nova tentativa
503 assessment-unavailable Sim Aguarde o Retry-After (hoje 1 s; pelo menos 1 s se ele faltar); nova tentativa. O estado pode ser indeterminado
410 assessment-result-expired Não O resultado antigo não está mais disponível. Pare as novas tentativas de reconciliação; preserve os IDs originais da operação e não regenere a operação
Erro de rede, timeout, resposta truncada ou resposta acima de 65536 bytes Sim Trate como indeterminado; nova tentativa com o mesmo corpo
Qualquer outro 4xx, ou 500 Não Corrija a causa (veja os erros)

Uma nova tentativa é sempre: mesmo corpo, request_id novo, timestamp atual e nova assinatura. Isso vale também para 429 e 503: o request_id da tentativa respondida pode já ter sido consumido. Faça novas tentativas um número limitado de vezes, nunca em loop.

Uma operação, duas tentativas
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

Depois de um timeout, a avaliação pode ter sido armazenada sem que a resposta chegasse até você. Uma nova tentativa com o mesmo corpo resolve isso: um resultado retido retorna 200; um resultado expirado retorna 410, que não admite nova tentativa. Use uma política de novas tentativas limitada — backoff, Retry-After e o prazo da sua operação de negócio. Esgotado esse prazo, aplique o seu próprio fallback e reconcilie depois, com o mesmo corpo.

Um corpo que traz um event_context_token só se reconcilia dessa forma se a primeira tentativa foi armazenada, ou enquanto o token ainda estiver antes do expiresAt dele (no máximo a janela de evidência da sua aplicação, 60 ou 300 segundos, depois da coleta dele). Caso contrário, o mesmo corpo retorna 400 event-context-invalid; em vez disso, avalie o evento como uma nova operação com null (orientações de integração).

Não existe GET de recuperação: a recuperação é o mesmo POST.