Eventos de webhook
Referência completa de cada tipo de evento de webhook da Certivant — esquema do envelope, formatos de payload por evento, padrão de idempotência e regras de ordenação.
Todo payload de webhook compartilha o mesmo envelope externo. Apenas o bloco data muda de acordo com o tipo de evento.
{
"event": "kyc.request.approved",
"requestId": "req_01J...",
"organizationId": "org_01H...",
"externalRef": "user-9281",
"subTenantName": "Acme Corp",
"subTenantExternalId": "cust_8842",
"status": "APPROVED",
"timestamp": "2026-05-27T10:04:21.000Z",
"data": { /* event-specific */ }
}
| Campo | Tipo | Observações |
|---|---|---|
event | string | O tipo do evento. Sempre começa com kyc. |
requestId | string | O ID da requisição na Certivant |
organizationId | string | A organização dona da requisição |
externalRef | string | null | A chave estrangeira que você definiu na criação |
subTenantName | string | null | Modo parceiro: o nome de exibição do cliente final definido na requisição, se houver |
subTenantExternalId | string | null | Modo parceiro: o ID do seu cliente final definido na requisição — direcione o evento ao cliente certo sem precisar de uma consulta |
status | enum | Status atual da requisição no momento do evento |
timestamp | string (ISO-8601) | Quando o evento foi colocado na fila |
data | object | null | Payload específico do evento (veja abaixo) |
Catálogo de eventos
kyc.webhook.test
Uma verificação de conectividade — enviada quando você clica em Test em uma URL de webhook (em todo o locatário, em KYC → Configurações → Desenvolvedor, ou por formulário, no editor de formulários). Não é um evento de verificação real: não traz requestId e não altera nada do seu lado. Seu único propósito é confirmar que o seu endpoint está acessível.
Ele inclui um campo explícito de primeiro nível test: true, para que o seu manipulador possa encerrar o processamento e responder 2xx imediatamente — sem consultar uma requisição, verificar o status ou sequer validar a assinatura (não há nada a proteger, já que um ping não altera nenhum estado).
{
"event": "kyc.webhook.test",
"test": true,
"organizationId": "org_...",
"formId": "form_...", // present only for the per-form test
"formName": "KYC — Identity Verification",
"timestamp": "...",
"data": { "message": "This is a test webhook from your KYC platform." }
}
Manipulador recomendado:
const payload = JSON.parse(rawBody);
// Reachability ping — ack immediately, no auth needed.
if (payload.test === true || payload.event === "kyc.webhook.test") {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
// …verify the signature and process real events below…
Os eventos de resultado reais (com um status do ciclo de vida) ainda devem ter a assinatura verificada antes de você agir sobre eles — veja Verificação de assinatura. O evento de teste também é assinado (quando um segredo está definido), com o mesmo cabeçalho X-Certivant-Signature-V2 dos eventos reais e um timestamp em segundos Unix — então você pode verificá-lo, mas não é obrigatório.
O botão Enviar evento de teste do painel também pode enviar um payload de exemplo de kyc.request.created, kyc.request.approved, kyc.request.rejected ou kyc.request.escalated. Os exemplos têm o formato real do envelope, trazem "test": true, usam um requestId falso (req_sample_…) e data.sample: true, e nunca são armazenados como eventos reais. Não aja sobre eles em caminhos de código de produção: teste primeiro payload.test === true.
kyc.request.created
É disparado logo depois que POST /api/v1/kyc/requests retorna 201. Útil se você quiser confirmar o recebimento sem esperar que o solicitante envie os dados.
{
"event": "kyc.request.created",
"requestId": "req_...",
"organizationId": "org_...",
"externalRef": "user-9281",
"status": "PENDING_UPLOAD",
"timestamp": "...",
"data": null
}
kyc.request.processing
É disparado no momento em que a requisição passa para PROCESSING — ou seja, quando o solicitante (por meio de uma sessão hospedada ou do envio de um formulário) ou você (por meio de POST /requests/:id/submit) a submete. É o sinal de "enviado": ele chega antes de qualquer decisão, então o seu aplicativo pode mudar o usuário para o estado "em análise" assim que ele terminar de enviar os arquivos.
{
"event": "kyc.request.processing",
"requestId": "req_...",
"externalRef": "user-9281",
"status": "PROCESSING",
"timestamp": "...",
"data": null
}
Use-o quando quiser mostrar o progresso ("Sua verificação está em análise") entre a submissão e o evento final approved / rejected / escalated. Ele traz externalRef e os campos de subcliente, então você pode direcioná-lo ao usuário certo sem precisar fazer uma consulta.
kyc.document.uploaded
É disparado sempre que um documento é anexado a uma requisição. Útil se o seu aplicativo exibe um indicador em tempo real de "documentos recebidos até agora".
{
"event": "kyc.document.uploaded",
"requestId": "req_...",
"status": "PENDING_UPLOAD",
"data": {
"documentId": "doc_...",
"documentType": "GOVERNMENT_ID",
"side": "front",
"mimeType": "image/jpeg",
"fileSize": 412938
}
}
kyc.check.completed
É disparado quando cada verificação termina. Uma única requisição pode gerar vários desses eventos — um para a autenticidade do documento, um para a comparação facial, um para a prova de vida, e assim por diante.
{
"event": "kyc.check.completed",
"requestId": "req_...",
"status": "PROCESSING",
"data": {
"checkType": "face_match",
"result": "pass",
"score": 0.91,
"details": { "method": "deep_face_match" }
}
}
Valores possíveis de checkType: document_authenticity, ocr, face_match, liveness, sanctions_screening, pep_screening, adverse_media, proof_of_address, phone_verification, email_verification, registry_lookup (KYB).
kyc.request.approved
O principal: a requisição foi aprovada. É disparado tanto quando o mecanismo automatizado a libera quanto quando um revisor humano aprova manualmente uma requisição encaminhada (ou anteriormente rejeitada automaticamente), pelo painel ou pela API /decision. Em aprovações manuais, data.decision é "approve" e data.reason traz a nota do revisor; os campos detalhados do mecanismo (score, factors, …) estão presentes somente nas decisões automatizadas.
{
"event": "kyc.request.approved",
"requestId": "req_...",
"status": "APPROVED",
"data": {
"score": 94,
"decision": "approve",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.96 },
{ "name": "face_match", "weight": 0.3, "score": 0.91 },
{ "name": "liveness", "weight": 0.2, "score": 0.98 },
{ "name": "sanctions_screening", "weight": 0.2, "score": 1.0 }
],
"document": {
"type": "GOVERNMENT_ID",
"issuingCountry": "GB",
"expiryDate": "2031-04-12",
"isExpired": false
},
"extracted": {
"fullName": "Alex Singh",
"dateOfBirth": "1988-11-03",
"documentNumber": "GBR-1234567",
"address": "1 Example Street, London",
"nationality": "GB"
},
"liveness": { "isLive": true, "confidence": 0.98 },
"faceMatch": { "isMatch": true, "confidence": 0.91 },
"sanctions": { "isMatch": false, "topScore": 0, "listsChecked": ["OFAC", "UN", "EU", "UK_HMT"] }
}
}
O bloco data.extracted é uma mina de ouro — salve esses valores no registro do usuário para não pedir ao solicitante os mesmos dados duas vezes.
kyc.request.rejected
A requisição foi rejeitada — seja pelo mecanismo automatizado (veja data.disqualifiers e data.factors para os motivos), seja por um revisor humano que rejeitou manualmente uma requisição encaminhada (data.decision = "reject", data.reason = a nota do revisor).
{
"event": "kyc.request.rejected",
"requestId": "req_...",
"status": "REJECTED",
"data": {
"score": 18,
"decision": "reject",
"disqualifiers": ["document_expired", "liveness_failed"],
"reviewTriggers": [],
"reason": "Document expired (2024-01-01) and liveness confidence below threshold (0.22).",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.0, "verdict": "fail" },
{ "name": "liveness", "weight": 0.2, "score": 0.22, "verdict": "fail" }
],
"document": {
"type": "GOVERNMENT_ID",
"expiryDate": "2024-01-01",
"isExpired": true
},
"liveness": { "isLive": false, "confidence": 0.22 }
}
}
Você pode reenviar a requisição por meio de POST /requests/:id/resubmit, se a sua política permitir.
kyc.request.escalated
O mecanismo não teve confiança suficiente — pontuações limítrofes, correspondência parcial de sanções, documento de baixa qualidade, ou qualquer caso que tenha ultrapassado o limiar de encaminhamento da sua organização. Uma pessoa da sua organização precisa revisar.
{
"event": "kyc.request.escalated",
"requestId": "req_...",
"status": "ESCALATED",
"data": {
"score": 62,
"decision": "escalate",
"reviewTriggers": ["sanctions_partial_match", "low_face_match_confidence"],
"factors": [...],
"sanctions": {
"isMatch": true,
"topScore": 0.72,
"listsChecked": ["OFAC", "UN", "EU", "UK_HMT"]
},
"faceMatch": { "isMatch": true, "confidence": 0.68 }
}
}
Um revisor abre a requisição no painel e decide. Quando isso acontece, você recebe um segundo webhook — kyc.request.approved ou kyc.request.rejected — com a decisão humana registrada no registro de auditoria.
kyc.request.cancelled
É disparado quando uma requisição é cancelada explicitamente (atualmente somente pelo painel — não há endpoint de API).
{
"event": "kyc.request.cancelled",
"requestId": "req_...",
"status": "CANCELLED",
"data": { "reason": "applicant_withdrew", "cancelledBy": "user_..." }
}
Eventos de verificação ao vivo — conference.*
Os eventos de verificação de identidade ao vivo (sessão de vídeo ao vivo conduzida por uma pessoa) usam um corpo próprio, no formato de sessão — conferenceId, kycRequestId, applicantId, organizationId, o seu externalRef, o status da sessão e os carimbos de data e hora — em vez do envelope kyc.request.* acima. Eles são disparados tanto para sessões agendadas pelo painel quanto pela API.
| Evento | É disparado quando |
|---|---|
conference.scheduled | Uma verificação ao vivo é criada. |
conference.rescheduled | Ela é movida para um novo horário (previousScheduledFor). |
conference.cancelled | Ela é cancelada — pela API, ou porque o solicitante recusou o consentimento (cancellation). |
conference.started | O anfitrião inicia a sessão (uma vez a cada início). |
conference.completed | O anfitrião registra a decisão (decision.decision, decision.outcome, notas, indicador de gravação, certificado). |
{
"event": "conference.completed",
"conferenceId": "cmf...",
"kycRequestId": "req_...",
"applicantId": "cma...",
"organizationId": "org_...",
"externalRef": "order-10042",
"status": "completed",
"decision": { "decision": "verified", "outcome": "approved", "requestStatus": "APPROVED", "notes": "..." },
"timestamp": "..."
}
Payload completo e API de agendamento: API de verificação ao vivo.
Idempotência
A Certivant pode entregar o mesmo evento mais de uma vez (falhas de rede, novas tentativas — veja Novas tentativas). Torne o seu manipulador idempotente.
O cabeçalho X-Certivant-Event-Id (também enviado como X-KYC-Event-Id) é único por evento. Padrão comum:
async function handleWebhook(rawBody: string, headers: Headers) {
const eventId = headers.get("X-KYC-Event-Id")!;
// Skip if already processed
const seen = await db.webhookEvent.findUnique({ where: { id: eventId } });
if (seen) return;
const payload = JSON.parse(rawBody);
// Do the work + record the event in a single transaction
await db.$transaction([
db.webhookEvent.create({ data: { id: eventId, eventType: payload.event } }),
...yourBusinessLogic(payload),
]);
}
X-KYC-Event-Id é estável entre as novas tentativas — o mesmo evento lógico mantém o mesmo ID, não importa quantas tentativas de entrega ocorram.
Ordenação
Eventos de requisições diferentes podem chegar em qualquer ordem. Eventos da mesma requisição são colocados na fila na ordem em que foram gerados, mas as novas tentativas podem intercalá-los — por exemplo, um evento processing preso pode chegar depois de um evento approved da mesma requisição.
Padrão defensivo: use sempre o campo status como fonte de verdade e ignore os eventos cujo status seja "mais antigo" do que o que você já registrou. O ciclo de vida segue estritamente em frente (PENDING_UPLOAD → PROCESSING → APPROVED/REJECTED/ESCALATED), então, se o seu banco de dados indica que a requisição está APPROVED e chega um evento kyc.request.processing atrasado, descarte-o.
Testar os manipuladores de eventos localmente
Dois botões do painel ajudam nisso:
- Test (ao lado de qualquer URL de webhook, em todo o locatário ou por formulário) envia um ping de conectividade
kyc.webhook.test(test: true). Use-o para confirmar que o seu endpoint está acessível e retorna2xx— ele não precisa de dados de requisição nem de um segredo configurado. - Enviar evento de teste envia um payload completo de
kyc.request.approvedcom dados de exemplo, para que você possa exercitar o seu caminho real de tratamento de resultados.
Para testar outros tipos de evento, escreva um pequeno script que monte o mesmo envelope e faça um POST para a sua URL de desenvolvimento usando um túnel:
# tunnel localhost:3000 to a public URL
ngrok http 3000
# Then in the dashboard webhook settings, paste the ngrok URL,
# trigger real test requests via the API, and watch your logs.
Verificação de assinatura
Verifique o cabeçalho X-Certivant-Signature-V2 (HMAC-SHA256 sobre timestamp.corpo, com tolerância de 300 s) para confiar nos payloads de webhook. Inclui exemplos em Node, Python, Go, Ruby e PHP.
Novas tentativas e entrega
Como a Certivant repete entregas de webhook que falharam — cronograma exato de espera, regra de timeout de 10 segundos, recuperação de eventos esgotados e um padrão de manipulador rápido para ficar dentro do orçamento.