Pular para o conteúdo principal

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.

typeverdictDisparado quando
verification.approvedapprovedConfiança igual ou acima do limiar de aprovação, sem falhas graves, sem correspondência em listas de sanções
verification.rejectedrejectedConfiança abaixo do limiar de rejeição
verification.review_requiredreviewQualquer 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çaVeredicto
>= 90approved
6089.99review
< 60rejected

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 — ou rejected se 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.

CampoTipoSempre presenteDescrição
idstringSimevt_<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.
typestringSimO tipo do evento. É este o campo do seu switch.
createdAtnumberSimTimestamp Unix em segundos (inteiro), de quando o evento foi enfileirado. Não é ISO 8601.
tenantIdstringSimO ID do seu tenant.
verificationIdstringSimO ID vindo de /v1/verify/init (vf_*).
envstringSim"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".
verdictstringSimapproved, review ou rejected.
confidencenumberSimScore geral ponderado, de 0 a 100.
userRefstring | nullSimO userRef que você passou para /init, ou null se você não passou nenhum.
scoresobjectSimDetalhamento por sinal. Veja abaixo.
flagsarraySimObjetos no formato { level, text }. Veja abaixo.
fieldsExtractedobjectSimDados de identidade lidos do documento. PII. Veja abaixo.
latencyMsnumber | nullSimTempo 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.

ChaveFaixaSignificado
ocr_confidence0–100A confiança do próprio modelo de extração no texto do documento que ele leu
face_match0–100Similaridade biométrica entre a selfie e a foto do documento
liveness0–100 ou nullAnti-spoofing passivo na selfie. null quando nenhum sinal estava disponível ou o modelo deu erro
doc_quality0–100Nitidez, reflexo, moiré e resolução da imagem do documento
mrz_valid0–100Validade dos checksums da zona de leitura mecânica (MRZ)
name_match0–100Correspondê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.

levelSignificado
okUma checagem passou. Informativo, positivo.
warnUm problema leve. Contribui para uma confiança menor.
errUm 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 flags nã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:

textNívelSignificado
auto_approved_all_checks_passedokTudo passou; presente em toda aprovação automática
mrz_checksums_validokChecksums da MRZ verificados
mrz_viz_consistentokA MRZ concorda com os campos impressos do documento
active_liveness_liveokO desafio de prova de vida (liveness) ativa foi cumprido
image_blurrywarnImagem do documento sem nitidez suficiente para leitura confiável
heavy_glarewarnReflexo forte obscurecendo o documento
possible_screen_capturewarnPadrão moiré — o "documento" pode ser uma foto de uma tela
low_resolutionwarnImagem do documento abaixo da resolução utilizável
mrz_checksum_failedwarnMRZ presente, mas os checksums não validam
aml_possible_matchwarnCorrespondência fraca contra uma lista de sanções
missing_imageserrImagens obrigatórias ausentes no momento do processamento
no_face_detected_on_documenterrNenhum rosto encontrado na foto do documento
no_face_detected_on_selfieerrNenhum rosto encontrado na selfie
face_match_below_critical_thresholderrSelfie e foto do documento estão muito distantes
active_liveness_spooferrO desafio de prova de vida (liveness) ativa indica um replay ou uma superfície plana
document_quality_unusableerrImagem do documento inutilizável para verificação
mrz_viz_mismatcherrMRZ com checksum válido contradiz os campos impressos — sinal de falsificação
aml_sanctions_matcherrCorrespondê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.

Não mostre as flags ao usuário final

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.

ChaveExemplo
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