Tipos de evento
A Veridia emite três tipos de evento, todos eles resultados de verificação. Todo evento tem o mesmo formato; só mudam type, verdict e os valores.
type | verdict | Disparado quando |
|---|---|---|
verification.approved | approved | Confiança igual ou acima do limiar de aprovação, sem falhas graves, sem correspondência em listas de sanções |
verification.rejected | rejected | Confiança abaixo do limiar de rejeição |
verification.review_required | review | Qualquer coisa entre os dois, ou qualquer falha grave, ou uma correspondência forte em listas de sanções |
Não existem outros tipos. Não existe verification.created, verification.expired nem verification.refunded. Ainda assim, você deve tratar um type não reconhecido sem lançar erro — registre no log e retorne 2xx —, mas não construa lógica em cima de um nome que não aparece na tabela acima.
Como o veredicto é decidido
Os limiares são configurações de ambiente válidas para toda a implantação, não configuração por tenant. Não há um botão que o suporte possa ajustar para a sua conta.
| Confiança | Veredicto |
|---|---|
>= 90 | approved |
60 – 89.99 | review |
< 60 | rejected |
Duas regras se sobrepõem ao score, e ambas existem para falhar em direção a um humano, e não em direção a uma aprovação:
- Qualquer falha grave nunca aprova automaticamente. Nenhum rosto encontrado no documento ou na selfie, correspondência facial abaixo do limiar crítico, um spoof de prova de vida (liveness) ativa, um checksum de MRZ inválido, uma imagem de documento inutilizável, ou uma MRZ que contradiz os campos impressos do documento. Independentemente da confiança, o caso vira
review— ourejectedse a confiança também estiver abaixo de 60. - Uma correspondência forte em listas de sanções força
review. Ela rebaixa um caso que de outra forma seria aprovado e nunca rejeita automaticamente. A identidade pode ser perfeitamente válida; uma pessoa precisa liberá-la.
A consequência prática: um caso com score 87 é review, não approved. Se você planejou seu funil em torno de "confiança alta significa onboarding automático", dimensione a fila manual de acordo.
Campos comuns
Todo evento carrega exatamente estes campos.
| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
id | string | Sim | evt_<32 hex>. A chave de idempotência — estável nas reentregas deste evento, única entre eventos. Também enviada no cabeçalho Veridia-Event-Id. |
type | string | Sim | O tipo do evento. É este o campo do seu switch. |
createdAt | number | Sim | Timestamp Unix em segundos (inteiro), de quando o evento foi enfileirado. Não é ISO 8601. |
tenantId | string | Sim | O ID do seu tenant. |
verificationId | string | Sim | O ID vindo de /v1/verify/init (vf_*). |
env | string | Sim | "live" ou "test". Chaves de teste e de produção entregam na MESMA URL, então ramifique sobre isso antes de agir — um verification.approved sintético de uma rodada de QA nunca deve ativar uma conta real. Eventos enfileirados antes de 2026-07-30 são anteriores ao campo; trate um valor ausente como "live". |
verdict | string | Sim | approved, review ou rejected. |
confidence | number | Sim | Score geral ponderado, de 0 a 100. |
userRef | string | null | Sim | O userRef que você passou para /init, ou null se você não passou nenhum. |
scores | object | Sim | Detalhamento por sinal. Veja abaixo. |
flags | array | Sim | Objetos no formato { level, text }. Veja abaixo. |
fieldsExtracted | object | Sim | Dados de identidade lidos do documento. PII. Veja abaixo. |
latencyMs | number | null | Sim | Tempo do pipeline em milissegundos. null para eventos produzidos pela decisão de um revisor humano, onde a latência do pipeline não faria sentido. |
Campos que o payload não contém, apesar de parecerem plausíveis: event, metadata, submittedAt, completedAt, status. O metadata que você talvez tenha enviado para /v1/verify/submit é armazenado junto com a verificação, mas não é devolvido no evento — userRef, definido em /init, é o único campo de correlação que volta.
O único timestamp é createdAt, em segundos. Se você persiste uma data de conclusão de KYC, derive-a dele:
const kycCompletedAt = new Date(payload.createdAt * 1000);
scores
Seis chaves, todas em snake_case. O objeto tem o mesmo formato nos três tipos de evento.
| Chave | Faixa | Significado |
|---|---|---|
ocr_confidence | 0–100 | A confiança do próprio modelo de extração no texto do documento que ele leu |
face_match | 0–100 | Similaridade biométrica entre a selfie e a foto do documento |
liveness | 0–100 ou null | Anti-spoofing passivo na selfie. null quando nenhum sinal estava disponível ou o modelo deu erro |
doc_quality | 0–100 | Nitidez, reflexo, moiré e resolução da imagem do documento |
mrz_valid | 0–100 | Validade dos checksums da zona de leitura mecânica (MRZ) |
name_match | 0–100 | Correspondência aproximada entre submittedFullName e o nome lido do documento |
liveness é o único membro que pode ser nulo, e é justamente aquele sobre o qual os integradores mais costumam aplicar um limiar. Proteja-o:
const liveness = payload.scores.liveness;
if (liveness !== null && liveness < 50) {
// trate como um sinal fraco
}
Um liveness ausente não é um liveness aprovado. Se a sua política de risco depende dele, trate null como "desconhecido" e roteie de acordo, em vez de assumir um número padrão.
confidence é uma combinação ponderada desses sinais, limitada pelas regras de falha grave descritas acima. Não é a média.
flags
Um array de objetos — não de strings:
"flags": [
{ "level": "warn", "text": "heavy_glare" },
{ "level": "err", "text": "mrz_viz_mismatch" }
]
Níveis
São três, e ok é o que surpreende as pessoas.
level | Significado |
|---|---|
ok | Uma checagem passou. Informativo, positivo. |
warn | Um problema leve. Contribui para uma confiança menor. |
err | Um problema grave. Impede a aprovação automática de imediato. |
Não existe info nem critical. Dois modos de falha decorrem diretamente disso:
- Filtrar por
level === 'critical'não casa com nada, então todo caso cai na sua fila com prioridade normal — inclusive os que têm falhas graves, que são exatamente os que um revisor deveria ver primeiro. Filtre por'err'. - Tratar um array
flagsnão vazio como "algo está errado" gera alarme em caso de sucesso. Toda verificação aprovada automaticamente carrega{ "level": "ok", "text": "auto_approved_all_checks_passed" }como sua primeira flag. Um evento aprovado nunca éflags: [].
const hasHardFailure = payload.flags.some(f => f.level === 'err');
const problems = payload.flags.filter(f => f.level !== 'ok');
Valores de flag
Valores de text que o pipeline emite hoje:
text | Nível | Significado |
|---|---|---|
auto_approved_all_checks_passed | ok | Tudo passou; presente em toda aprovação automática |
mrz_checksums_valid | ok | Checksums da MRZ verificados |
mrz_viz_consistent | ok | A MRZ concorda com os campos impressos do documento |
active_liveness_live | ok | O desafio de prova de vida (liveness) ativa foi cumprido |
image_blurry | warn | Imagem do documento sem nitidez suficiente para leitura confiável |
heavy_glare | warn | Reflexo forte obscurecendo o documento |
possible_screen_capture | warn | Padrão moiré — o "documento" pode ser uma foto de uma tela |
low_resolution | warn | Imagem do documento abaixo da resolução utilizável |
mrz_checksum_failed | warn | MRZ presente, mas os checksums não validam |
aml_possible_match | warn | Correspondência fraca contra uma lista de sanções |
missing_images | err | Imagens obrigatórias ausentes no momento do processamento |
no_face_detected_on_document | err | Nenhum rosto encontrado na foto do documento |
no_face_detected_on_selfie | err | Nenhum rosto encontrado na selfie |
face_match_below_critical_threshold | err | Selfie e foto do documento estão muito distantes |
active_liveness_spoof | err | O desafio de prova de vida (liveness) ativa indica um replay ou uma superfície plana |
document_quality_unusable | err | Imagem do documento inutilizável para verificação |
mrz_viz_mismatch | err | MRZ com checksum válido contradiz os campos impressos — sinal de falsificação |
aml_sanctions_match | err | Correspondência forte contra uma lista de sanções; força review, nunca rejeita automaticamente |
Mais duas observações. Erros de correspondência facial são expostos como flags err cujo text é a string do erro subjacente, então trate esta lista como o conjunto de valores conhecidos, e não como um enum fechado — case com o que você reconhece e repasse o restante aos seus revisores literalmente. E as duas flags de AML são as que têm peso regulatório: aml_sanctions_match significa que uma pessoa precisa liberar o caso antes do onboarding, por melhor que tenha sido a biometria.
Dizer a alguém qual sinal o pegou é um tutorial de graça para a próxima tentativa. Mostre um genérico "não conseguimos verificar seu documento, entre em contato com o suporte" e guarde flags para a sua fila interna de revisão.
fieldsExtracted
Os dados de identidade lidos do documento.
| Chave | Exemplo |
|---|---|
full_name | "MARIA ELENA GONZALEZ" |
document_number | "4567890" |
date_of_birth | "1991-04-17" |
nationality | "PRY" |
document_type | "dni" |
Qualquer valor pode ser null — o pipeline reporta o que conseguiu ler. São saídas de OCR e MRZ, não afirmações que a Veridia faz sobre a pessoa. nationality em particular é lido do documento e frequentemente está ausente ou errado no tráfego real; não o use para dirigir lógica importante sem corroboração.
Este objeto é dado pessoal. Seu endpoint precisa ser HTTPS (o painel obriga), e se você loga corpos brutos, seus logs agora contêm documentos de identidade.
Exemplos
verification.approved
{
"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
}
case 'verification.approved':
await db.users.update(payload.userRef, {
kycStatus: 'verified',
kycCompletedAt: new Date(payload.createdAt * 1000),
kycVerificationId: payload.verificationId,
});
await sendWelcomeEmail(payload.userRef);
break;
verification.rejected
{
"id": "evt_3b71dd90c4a24f6ea5c0812fb7e39d14",
"type": "verification.rejected",
"createdAt": 1753145532,
"tenantId": "tn_default_demo",
"verificationId": "vf_BX18DEXSGFRX5U16YH3Q",
"verdict": "rejected",
"confidence": 42.1,
"userRef": "customer-67890",
"scores": {
"ocr_confidence": 65.0,
"face_match": 31.4,
"liveness": 88.0,
"doc_quality": 50.5,
"mrz_valid": 0.0,
"name_match": 44.0
},
"flags": [
{ "level": "err", "text": "face_match_below_critical_threshold" },
{ "level": "warn", "text": "image_blurry" }
],
"fieldsExtracted": {
"full_name": "J. PEREZ",
"document_number": null,
"date_of_birth": null,
"nationality": null,
"document_type": "dni"
},
"latencyMs": 2971
}
case 'verification.rejected':
await db.users.update(payload.userRef, {
kycStatus: 'rejected',
kycRejectionFlags: payload.flags, // apenas uso interno
});
await sendGenericFailureEmail(payload.userRef);
break;
verification.review_required
{
"id": "evt_c05e8a1746bf4d92ae37b6c2019df8aa",
"type": "verification.review_required",
"createdAt": 1753149933,
"tenantId": "tn_default_demo",
"verificationId": "vf_CY29EFYTGFSZ6V27ZH4R",
"verdict": "review",
"confidence": 87.3,
"userRef": "customer-11111",
"scores": {
"ocr_confidence": 72.0,
"face_match": 79.5,
"liveness": null,
"doc_quality": 45.0,
"mrz_valid": 100.0,
"name_match": 91.0
},
"flags": [
{ "level": "warn", "text": "heavy_glare" },
{ "level": "err", "text": "document_quality_unusable" }
],
"fieldsExtracted": {
"full_name": "CARLOS ALBERTO RIVAS",
"document_number": "3312004",
"date_of_birth": "1988-11-02",
"nationality": null,
"document_type": "dni"
},
"latencyMs": 3402
}
Repare no formato deste: a confiança é 87,3 — acima do que um integrador poderia supor ser uma aprovação — e liveness é null. Foi a flag err que o colocou em revisão.
case 'verification.review_required':
await db.users.update(payload.userRef, { kycStatus: 'pending_review' });
await reviewQueue.add({
verificationId: payload.verificationId,
userRef: payload.userRef,
flags: payload.flags,
priority: payload.flags.some(f => f.level === 'err') ? 'high' : 'normal',
});
break;
review é terminal até que uma pessoa aja sobre ele. Nenhum outro evento chega por conta própria. Quando um revisor decide o caso no painel da Veridia, você recebe um segundo evento — verification.approved ou verification.rejected — para o mesmo verificationId, com um id novo. Seu handler precisa aplicá-lo. É por isso que a chave de deduplicação tem que ser id e não qualquer coisa derivada de verificationId.
Roteamento
async function handleVeridiaWebhook(payload) {
switch (payload.type) {
case 'verification.approved':
return handleApproved(payload);
case 'verification.rejected':
return handleRejected(payload);
case 'verification.review_required':
return handleReviewRequired(payload);
default:
// Tipo desconhecido: registre no log e retorne sucesso. Nunca lance
// exceção — uma exceção aqui vira um 5xx, e o evento é reentregue
// por ~12,6 minutos.
logger.warn('Unknown Veridia event type', {
type: payload.type,
eventId: payload.id,
verificationId: payload.verificationId,
});
}
}
O que vem a seguir
- Exemplos — implementações completas de handlers
- Verificação de assinatura — referência do algoritmo
- Reentregas — garantias de entrega
- Webhooks — de volta ao índice da seção