Idempotência
Idempotência permite retentar uma requisição de escrita depois de um timeout ou de uma queda de rede sem criar um segundo registro nem repetir o efeito. Na integração de KYT isso importa por um motivo concreto: cada análise concluída é registrada como uso da API, gera alerta quando sinalizada e dispara webhook. Uma repetição que não é reconhecida como repetição faz tudo isso duas vezes.
Onde o header é honrado
Seção intitulada “Onde o header é honrado”| Operação | Repetição com a mesma chave e o mesmo corpo | Mesma chave com corpo diferente | Chave inválida |
|---|---|---|---|
POST /vasp/screenings |
200 com a análise original e idempotent_replay: true |
409 com KYT_IDEMPOTENCY_CONFLICT |
400 com KYT_IDEMPOTENCY_INVALID |
POST /vasp/customers |
200 com o cliente original e idempotent_replay: true |
409 com KYT_IDEMPOTENCY_CONFLICT |
400 com invalid_idempotency_key |
POST /vasp/customers/batch |
207 com o recibo original e idempotent_replay: true |
409 com KYT_IDEMPOTENCY_CONFLICT |
400 com invalid_idempotency_key, inclusive chave ausente |
POST /vasp/wallets |
200 com a carteira original e idempotent_replay: true |
409 com KYT_IDEMPOTENCY_CONFLICT |
400 com invalid_idempotency_key |
O lote exige chave explícita e vincula o recibo também ao ator da API key; não troque de chave de API para reconciliar um lote pendente. A regra de bytes abaixo descreve o screening e os cadastros individuais. POST /vasp/kyt-report não oferece deduplicação por esse header. O anexo PATCH /vasp/screenings/{id}/tx-hash é idempotente pelo próprio hash: repetir o mesmo devolve replay, outro conflita. Não presuma idempotência global. PATCH /vasp/screenings/{id}/client-decision não precisa dele: a decisão do cliente é registrada uma única vez, e a segunda chamada responde 409 com KYT_CLIENT_DECISION_ALREADY_RECORDED. Se você retentou depois de um timeout e recebeu esse 409, leia a decisão gravada em GET /vasp/screenings/{id} antes de concluir que algo falhou.
O que conta como “mesmo corpo”
Seção intitulada “O que conta como “mesmo corpo””A comparação é feita sobre o resumo SHA-256 dos bytes enviados, não sobre o JSON interpretado. Reserializar o objeto com outra ordem de campos, outro espaçamento ou outra representação de número conta como corpo diferente e devolve 409. Guarde os bytes da primeira tentativa e reenvie exatamente esses bytes.
Formato da chave
Seção intitulada “Formato da chave”| Regra | Detalhe |
|---|---|
| Tamanho | Até 255 bytes, depois de remover espaços das pontas. Não há mínimo |
| Chave vazia | Equivale a não enviar o header |
| Caracteres | Caracteres de controle são recusados. Não há restrição a alfanumérico |
| Prefixo reservado | Chave que começa com snx-derived: é recusada: esse espaço pertence às chaves que o servidor deriva sozinho |
| Escopo | Por tenant e por operação. A mesma chave em POST /vasp/customers e em POST /vasp/wallets não colide |
| Validade | Não há expiração fixa. A chave declarada por você vale enquanto o registro que ela criou existir |
Use UUID v4 ou um identificador derivado do evento de negócio (pedido-12345-tentativa-1). Não use timestamp puro: duas tentativas legítimas em paralelo podem colidir.
Repetição de POST /vasp/screenings sem o header
Seção intitulada “Repetição de POST /vasp/screenings sem o header”Se você não enviar Idempotency-Key mas enviar client_transaction_id, o servidor deriva uma chave a partir da operação, do resumo do corpo e de uma janela de tempo de 10 minutos. Uma repetição com o mesmo client_transaction_id e os mesmos bytes, dentro de um intervalo de 10 a 20 minutos, devolve a análise original com idempotent_replay: true em vez de criar uma segunda.
Duas chamadas idênticas simultâneas nesse modo não são processadas em paralelo: a segunda recebe 409 com KYT_IDEMPOTENCY_IN_PROGRESS e retry_after_seconds: 2. Repita depois desse intervalo e ela recebe o replay.
Sem client_transaction_id e sem o header, não há como distinguir uma repetição de uma segunda operação real, e a análise é feita de novo. Esse limite é deliberado: deduplicar às cegas apagaria uma operação que pode ser legítima.
POST /vasp/customers e POST /vasp/wallets não derivam chave. Sem o header, POST /vasp/customers com um external_id já usado no tenant responde 409 com customer_already_exists, e POST /vasp/wallets com uma carteira que o tenant já conhece responde 200 com o resultado armazenado, sem criar outra.
Exemplo em POST /vasp/screenings
Seção intitulada “Exemplo em POST /vasp/screenings”Gere a chave uma vez por operação e guarde-a junto da operação. Numa repetição, reenvie a mesma chave; gerar outra a cada tentativa desliga a idempotência.
CHAVE="$(uuidgen)" # uma por operação, reutilizada em toda repetição dela
curl -X POST https://crypto-api.sentinexrisk.com/api/v1/vasp/screenings \ -H "X-API-Key: $SENTINEX_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CHAVE" \ --data-binary @analise.json--data-binary envia os bytes do arquivo sem alteração, que é o que torna a repetição reconhecível.
Repetição com a mesma chave e o mesmo corpo:
{ "success": true, "idempotent_replay": true, "data": { "screening_id": "…", "recommendation": "REVIEW" }}Mesma chave com corpo diferente:
{ "success": false, "error": { "code": "KYT_IDEMPOTENCY_CONFLICT", "http_status": 409, "message": "A Idempotency-Key foi reutilizada com outro corpo.", "action": "Use uma nova chave para outra intenção.", "retriable": false, "request_id": "…" }}Em POST /vasp/customers e POST /vasp/wallets o conflito usa o envelope curto, com error.code e error.message.
Retentativa recomendada
Seção intitulada “Retentativa recomendada”import { randomUUID } from 'node:crypto';
async function analisarComRetentativa(corpoEmBytes) { const chave = randomUUID(); // gerada UMA vez por intenção de negócio for (let tentativa = 1; tentativa <= 4; tentativa++) { const resposta = await fetch('https://crypto-api.sentinexrisk.com/api/v1/vasp/screenings', { method: 'POST', headers: { 'X-API-Key': process.env.SENTINEX_API_KEY, 'Content-Type': 'application/json', 'Idempotency-Key': chave, }, body: corpoEmBytes, // os MESMOS bytes em todas as tentativas }); if (resposta.ok || tentativa === 4) return resposta; const erro = (await resposta.clone().json().catch(() => ({}))).error ?? {}; const retentavel = resposta.status === 429 || resposta.status >= 500 || erro.code === 'KYT_IDEMPOTENCY_IN_PROGRESS'; if (!retentavel) return resposta; // 429 da chave traz Retry-After no header; os erros da análise trazem retry_after_seconds no corpo. const espera = Number(resposta.headers.get('Retry-After')) || erro.retry_after_seconds || 2 ** tentativa; await new Promise((r) => setTimeout(r, espera * 1000)); }}A chave não muda entre tentativas, e os bytes do corpo também não. É isso que torna a retentativa segura.
O que idempotência não faz
Seção intitulada “O que idempotência não faz”- Não impede que duas chaves diferentes criem dois registros. Para isso existe o
external_idúnico por tenant - Não atravessa tenants: a chave de um tenant não interfere em outro
- Não vale onde a operação não implementa o header. Confira a tabela do topo
Retry e nova medição são intenções diferentes
Seção intitulada “Retry e nova medição são intenções diferentes”Retry reconcilia a análise antiga: mesma chave e mesmos bytes. Reprocessamento após HOLD exige nova análise, com nova chave e rastreabilidade para o screening anterior no seu sistema. GET e replay não consultam novamente fontes nem reaplicam configuração alterada.