Webhooks
Um webhook é como você fica sabendo o resultado de uma verificação. Quando uma verificação chega a um veredicto — automaticamente, ou porque um revisor humano decidiu —, a Veridia faz um POST de um corpo JSON assinado para a URL que você configurou.
Esta é a única forma push de obter um veredicto. O evento veridia:complete do widget informa que o usuário terminou de enviar; ele não carrega o veredicto. GET /v1/verify/{id} carrega, mas exige uma chave secreta e exige que você faça polling.
Os três eventos
Existem exatamente três tipos de evento. Não existe verification.created, não existe verification.expired e não há como assinar apenas um subconjunto — um tenant recebe os três ou nenhum.
type | verdict | Significado |
|---|---|---|
verification.approved | approved | Liberado. Seguro fazer o onboarding. |
verification.rejected | rejected | Reprovado. Não faça o onboarding. |
verification.review_required | review | Um humano precisa olhar o caso. Não é um estado transitório — nenhum outro evento chega até que um revisor decida. |
Quando um revisor decide depois um caso em review, você recebe um segundo evento — verification.approved ou verification.rejected — para o mesmo verificationId, com um novo id. Esse segundo evento é o que desbloqueia o usuário. Handlers que colapsam os dois eventos em uma única chave de deduplicação descartam silenciosamente a decisão humana; veja Idempotência.
Um payload completo
O corpo é plano. Não existe um envelope data.
{
"id": "evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b",
"type": "verification.approved",
"createdAt": 1753142348,
"tenantId": "tn_default_demo",
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"verdict": "approved",
"confidence": 93.1,
"userRef": "customer-12345",
"scores": {
"ocr_confidence": 78.0,
"face_match": 96.2,
"liveness": 91.5,
"doc_quality": 85.0,
"mrz_valid": 100.0,
"name_match": 88.0
},
"flags": [
{ "level": "ok", "text": "auto_approved_all_checks_passed" },
{ "level": "ok", "text": "mrz_checksums_valid" }
],
"fieldsExtracted": {
"full_name": "MARIA ELENA GONZALEZ",
"document_number": "4567890",
"date_of_birth": "1991-04-17",
"nationality": "PRY",
"document_type": "dni"
},
"latencyMs": 3184
}
Referência completa campo a campo: Tipos de evento.
Três coisas que quebram integrações
Vale a pena ler antes de escrever o handler, porque cada uma delas falha silenciosamente.
1. O discriminador é type, não event
switch (payload.type) { /* ... */ } // correto
switch (payload.event) { /* ... */ } // sempre undefined → cai no default
Não existe uma chave event no corpo. Um switch sobre ela não casa com nada, seu handler devolve 200 OK e nenhum veredicto é aplicado. Usuários aprovados ficam pendentes para sempre, e os rejeitados também. Nada aparece nos seus logs de erro, porque nada deu erro.
2. Deduplique pelo id do evento
A entrega é ao menos uma vez. Um handler que processou um evento com sucesso mas demorou a responder vai recebê-lo de novo. Você precisa de uma chave de deduplicação, e a correta é id — o valor evt_*, que é estável em todos os reenvios do mesmo evento e único entre eventos diferentes.
const dedupKey = payload.id; // correto
const dedupKey = `${payload.verificationId}:${payload.type}`; // ERRADO
A segunda forma parece razoável e é justamente a armadilha. Uma verificação que vai para review_required e depois é aprovada por um revisor produz dois eventos com valores diferentes de type, então essa chave sobrevive — mas qualquer chave construída só a partir de verificationId, ou a partir de um campo que resulta em undefined, colapsa a decisão humana no evento anterior da máquina e a descarta. Use id. Ele também está disponível no cabeçalho Veridia-Event-Id, então você pode deduplicar antes de fazer o parse do corpo.
3. As chaves de scores são snake_case, e liveness pode ser null
payload.scores.face_match // 96.2
payload.scores.faceMatch // undefined
undefined < 70 é false em JavaScript, então um limiar escrito com a grafia camelCase nunca dispara — ele falha aberto, admitindo todo mundo. E scores.liveness é null quando não houve sinal de prova de vida (liveness) ou o modelo deu erro, então fazer aritmética com esse valor sem checar null lança exceção em produção.
fieldsExtracted é dado pessoal
fieldsExtracted carrega PII de identidade: nome completo, número do documento e data de nascimento, além de nacionalidade e tipo de documento. É o conteúdo transcrito de um documento oficial de identidade.
Duas consequências:
- Seu endpoint precisa ser HTTPS. O painel se recusa a salvar uma URL que não comece com
https://, justamente porque esse corpo, de outra forma, trafegaria pela rede em claro. Não há exceção para HTTP, nem mesmo para localhost — veja testes locais. - Seus logs agora são um repositório de dados pessoais. Se você loga os corpos brutos dos webhooks (um padrão normal e sensato para depuração), sua política de retenção de logs e seus controles de acesso agora se aplicam a documentos de identidade. Remova
fieldsExtractedantes de logar, ou assuma isso deliberadamente.
Valores podem ser null — o pipeline extrai o que consegue ler. São saídas de OCR/MRZ, não valores que a Veridia afirma serem verdadeiros.
Para onde ir agora
- Visão geral — configurar o endpoint, cabeçalhos e o padrão confirmar-depois-processar
- Verificação de assinatura — o algoritmo HMAC, com código funcional para quatro stacks
- Tipos de evento — cada campo, cada score, cada flag
- Reentregas — o cronograma de backoff, garantias de entrega e como recuperar um evento que falhou
- Exemplos — handlers completos para Express, FastAPI, Laravel e Cloudflare Workers