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-Timestampe a assinatura. Eles mudam a cada tentativa.
O que o servidor faz com a operação
Seção intitulada “O que o servidor faz com a operação”| 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.
{ "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}Quando fazer nova tentativa
Seção intitulada “Quando fazer nova tentativa”| 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.
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 nothingTimeout sem resposta
Seção intitulada “Timeout sem resposta”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.