Solicitações de verificação idempotentes: evitando verificações e cobranças duplicadas

Charles Archibong, Cofundador
Atribua um requestId a cada verificação lógica, salve-o antes de enviar a solicitação e reutilize-o em cada nova tentativa. Um requestId repetido retorna a verificação existente em vez de criar e cobrar uma segunda.

Pontos principais
- Uma solicitação repetida de metadata.requestId retorna a verificação existente em vez de criar uma nova.
- Gere o requestId a partir do seu próprio registro e armazene-o antes da primeira tentativa, não dentro do loop de repetição.
- Tente novamente os códigos 429, 502, 503, 504 e falhas de conexão com um período de espera; trate outras respostas 4xx como bugs a serem corrigidos.
- O código 202 significa apenas que o item está na fila. Uma falha na verificação é um resultado a ser tratado, não um erro que exija nova tentativa.
Para tentar novamente uma solicitação de verificação com segurança, envie o mesmo.metadata.requestIdtoda vez que você tentar novamente. Toden guloseimasrequestIdcomo sua chave de idempotência: se uma solicitação chegar com umrequestIdSua organização já utilizou o API Retorna a verificação existente em vez de criar uma segunda, e você não é cobrado duas vezes.
Esse único campo só te protege se você o utilizar corretamente.requestIdDeve ser gerado uma vez por verificação lógica, salvo antes da primeira tentativa e reutilizado integralmente. Gere-o dentro do seu loop de repetição e cada nova tentativa se tornará uma verificação completamente nova. O restante deste artigo aborda onde criá-lo, quais respostas devem ser repetidas e quais "falhas" não são falhas de fato.
O que dá errado sem idempotência?
O caso clássico é um tempo limite após uma operação bem-sucedida. Seu servidor enviaPOST /api/kyc/verify, o API O sistema aceita a solicitação e enfileira a verificação, mas a resposta se perde no caminho de volta: uma queda na conexão móvel, um tempo limite do balanceador de carga, uma reinicialização do pod. Do seu lado, a solicitação falhou. Do nosso, foi bem-sucedida.
Se o seu código tentar novamente com um novo identificador, a plataforma verá uma nova solicitação e criará uma segunda verificação. Em Produção, cada verificação concluída é debitada do seu saldo de crédito, de acordo com a documentação do ambiente (abre num novo separador) , portanto, a resposta perdida resultou em uma segunda verificação. Isso também deixa dois IDs de verificação vinculados a um mesmo solicitante, e sua equipe de reconciliação agora precisa determinar qual deles é o verdadeiro.
Com o mesmorequestId, a nova tentativa retorna a verificação original e a resposta perdida não tem custo.
Como funciona o requestId no endpoint de verificação?
Ele reside dentro da solicitação.metadataobjeto. Da referência de verificação de criação (abre num novo separador) :
{
"country": "NG",
"idType": "bvn",
"idNumber": "12345678901",
"externalUserId": "user_42",
"metadata": { "requestId": "order_1001", "loanId": "loan_20191" }
}O primeiro envio retorna202 Accepted:
{
"verificationId": "ver_01j9...",
"status": "processing",
"externalUserId": "user_42",
"metadata": { "loanId": "loan_20191" }
}Uma submissão posterior com o mesmorequestIdretorna essa verificação, qualquer que seja o estado em que ela se encontre, por exemplo{ "verificationId": "ver_01j9...", "status": "approved" }.externalUserIde seus metadados retornam exatamente como foram armazenados, de modo que a nova tentativa e a original coincidem.
As chaves são definidas de acordo com o escopo da sua organização. Se umarequestIdcolide com uma pertencente a outra organização, a API retornos403com{ "error": "Forbidden" }em vez de divulgar qualquer informação sobre essa verificação.
Onde você deve gerar o requestId?
Com base no seu próprio histórico, e antes de enviar qualquer coisa. Um padrão que se confirma:
Quando um cliente inicia a verificação de identidade, crie uma linha em seu próprio banco de dados para essa tentativa (uma solicitação, uma etapa de integração, um empréstimo).
Derive o
requestIddessa linha, por exemplokyc-application-8841e armazene-o na linha.Envie a solicitação com esse valor armazenado.
Em qualquer nova tentativa, leia o valor da linha de volta. Nunca calcule um novo.
Seguem-se três regras:
Não use um número aleatório. UUID gerado por HTTP chamada. Esse é um identificador único para a chamada, o que é o oposto do que você precisa.
Defina um namespace. Um simples...
1001É provável que entre em conflito com seus outros registros ao longo do tempo, e um conflito entre organizações retorna.403Um prefixo comokyc-application-torna o valor inequívoco.Crie uma nova verificação apenas para uma verificação genuinamente nova. Se um cliente for recusado e você decidir que ele pode começar do zero, essa é uma nova verificação lógica com uma nova solicitação.
requestIdReutilizar a versão antiga simplesmente retornaria a verificação recusada.
Se um avaliador enviar uma verificação para que o candidato possa refazer algumas etapas, você não precisa de uma nova.requestIdDe forma alguma. Uma verificação que é reenviada ou verificada novamente mantém seu ID, e o reenvio aparece como uma nova.attemptnele, conforme descrito na referência do resultado da verificação (abre num novo separador) .
Quando o SDK envia para você
A maioria das integrações não faz chamadas./verifydiretamente. Os SDKs da web, React Native e Flutter fazem a chamada quando o candidato clica em enviar. Eles aceitammetadatacom o seurequestId(metadata={{ requestId: "order_1001" }}Em React Native,metadata: {'requestId': 'order_1001'}Em Flutter, portanto, o mesmo princípio se aplica: passe um valor derivado do seu próprio registro, e não um gerado do zero cada vez que a tela é montada.
Quais respostas você deve tentar novamente?
A documentação sobre erros (abre num novo separador) é explícita e corresponde a uma tabela resumida.
Resposta | Tentar novamente? | O que fazer |
|---|---|---|
| Não | Feito. Acompanhe o |
| Sim | Aguarde o |
| Sim | Recuo exponencial com jitter, mesmo |
Reinicialização da conexão, DNS falha, tempo limite | Sim | Igual ao anterior. A solicitação pode ter sido bem-sucedida, e é exatamente por isso que... |
| Não cegamente | Guarde os detalhes da solicitação e entre em contato com o suporte se o problema persistir. |
| Não | Corrija a carga útil. Leia |
| Não | Verifique a chave e seu ambiente. |
| Não | A produção requer um perfil comercial aprovado. |
| Não | Seu |
O limite documentado é de 100 solicitações a cada 15 minutos por cliente, comRateLimit-Limit,RateLimit-RemainingeRateLimit-ResetCabeçalhos nas respostas. LeituraRateLimit-RemainingPermite que uma tarefa em lote seja processada mais lentamente antes de ser recusada, em vez de depois. A página de limites de taxa (abre num novo separador) observa que os limites podem ser diferentes entre os ambientes de Sandbox e Produção e podem ser ajustados para sua conta.
Como seria um loop de repetição seguro?
Uma versão mínima em Node.js, usando o globalfetchO corpo é serializado uma única vez, fora do loop, portanto, cada tentativa envia bytes idênticos com o mesmorequestId.
const VERIFY_URL = 'https://api.toden.co/api/kyc/verify';
const RETRYABLE = new Set([429, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoff = (attempt) => Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random());
export async function submitVerification(body, apiKey, maxAttempts = 5) {
if (!body.metadata?.requestId) throw new Error('metadata.requestId is required');
const payload = JSON.stringify(body);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let res;
try {
res = await fetch(VERIFY_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: payload,
});
} catch (networkError) {
if (attempt === maxAttempts) throw networkError;
await sleep(backoff(attempt));
continue;
}
if (res.ok) return res.json(); // 202 for a new verification, or the existing one on a replay
if (!RETRYABLE.has(res.status) || attempt === maxAttempts) {
const detail = await res.json().catch(() => ({}));
throw Object.assign(new Error(detail.error ?? `HTTP ${res.status}`), { status: res.status, detail });
}
const resetSeconds = Number(res.headers.get('RateLimit-Reset'));
await sleep(res.status === 429 && resetSeconds > 0 ? resetSeconds * 1000 : backoff(attempt));
}
}Observe o que está ausente: nenhuma nova tentativa em400,401ou403e nenhum novo identificador em lugar nenhum. O chamador fornece umrequestIdJá está armazenado.
Uma verificação falhada é algo que deve ser tentado novamente?
Não, e confundir os dois é uma fonte comum de duplicatas.202Isso significa apenas que a solicitação foi enfileirada. A verificação ainda pode ser concluída.declinedporque não houve correspondência facial ou porque o documento de identidade não foi encontrado no banco de dados do governo. Esses são resultados fornecidos por webhook ou por votação, cada um com umreasone um estávelreasonCodeEles não são. HTTP erros.
Se a sua lógica de repetição monitora os resultados e reenvia em caso de recusa, ela receberá a mesma verificação recusada de volta (a mesma).requestId) ou criar uma segunda verificação paga da mesma entrada incorreta (nova)requestIdNenhuma das duas ajuda. Ramifique-se noreasonCodeEm vez disso: peça uma nova foto emdocument_blurry, para um número corrigido emidentity_not_foundE, deliberadamente, submeter a pessoa a uma nova verificação ou a uma verificação refeita.
O lado do webhook também precisa de idempotência?
Sim. A mesma entrega de webhook pode chegar mais de uma vez, pois uma nova tentativa de entrega é feita até que seu endpoint a confirme. A documentação de webhooks (abre num novo separador) recomenda a remoção de duplicatas noX-Myaza-Deliverycabeçalho, ou emverificationIdRegistre o que você processou e defina o manipulador como uma operação nula na segunda vez. O envio e o recebimento idempotentes, juntos, são o que tornam o fluxo exatamente uma vez do ponto de vista do seu cliente.
Lista de verificação
Um
requestIdpor verificação lógica, derivada do seu próprio registro e com namespace.Armazenado antes da primeira tentativa; lido novamente, nunca regenerado, em uma nova tentativa.
Passou pelo SDK 's
metadataquando o SDK submete em seu nome.Tentativas de relançamento
429,502,503,504e apenas falhas de rede, com recuo e oRateLimit-Resetvalor.Resultados como
declinedtratado porreasonCode, nunca por meio de um novo envio.O manipulador de webhook foi desduplicado em
X-Myaza-DeliveryouverificationId.
O contrato de solicitação completo está na referência de verificação de criação (abre num novo separador) , e o hub do desenvolvedor contém os links para o restante. API .
Fontes

Charles Archibong
Cofundador
Charles Archibong é cofundador da Toden. Ele escreve sobre verificação de identidade, tecnologia financeira e o trabalho prático de construção de serviços digitais confiáveis.


