CPFHub.io

Webhook da Verificação de Identidade

Cada decisão de uma Verificação de Identidade, automática ou feita pela sua equipe no painel, gera um POST para a URL que você cadastrou. O evento não traz dados pessoais: só IDs, a decisão e os motivos.

Configurar

No painel, em Verificações > Configurações > Integrações:

  1. Cadastre a URL do seu endpoint. Só https e só endereços públicos.
  2. Copie o segredo e guarde numa variável de ambiente (ex.: CPFHUB_WEBHOOK_SECRET).
  3. Use o envio de teste para receber um evento kyc.test e conferir a assinatura.

Evento

JSON
{
  "event": "kyc.decision",
  "eventId": "3e8a1f20-5c4b-4d9e-8f7a-6b2c1d0e9f8a",
  "verificationId": "8f3c2a1e-77b0-4d3c-9a1f-0e5b6c7d8e9f",
  "linkId": "5b1d7c2e-8a4f-4e7b-9c3d-2f6a1e0b9d47",
  "externalId": "cliente-4821",
  "status": "approved",
  "previousStatus": null,
  "decidedBy": "automatic",
  "reasons": [],
  "occurredAt": "2026-10-27T14:06:12.000Z"
}
CampoTipoDescrição
eventstringkyc.decision, ou kyc.test no envio de teste
eventIdstringID do evento
verificationIdstringID da verificação
linkIdstring | nullO id devolvido por POST /kyc/links
externalIdstring | nullO identificador que você enviou ao criar o link
statusstringDecisão: approved, manual_review ou rejected. Veja Decisão.
previousStatusstring | nullA decisão anterior, quando ela mudou (por exemplo, de manual_review para approved)
decidedBystringautomatic (regras da conta) ou manual (sua equipe no painel)
reasonsarrayMotivos da decisão automática: [{ "code", "message" }]. Vazio em decisões manuais.
occurredAtstringQuando a decisão foi tomada (ISO 8601, UTC)

Cabeçalhos

HeaderDescrição
x-cpfhub-eventO mesmo valor de event
x-cpfhub-deliveryID da entrega. Use para ignorar repetições.
x-cpfhub-signaturet=<unix>,v1=<hex>: momento do envio em segundos e a assinatura

Verificar a assinatura

v1 é o HMAC-SHA256, em hexadecimal, de "<t>.<corpo cru>" com o seu segredo. Calcule sobre o corpo exatamente como chegou, antes de qualquer parse de JSON, compare em tempo constante e recuse eventos com t a mais de 5 minutos do seu relógio.

import crypto from 'node:crypto'
import express from 'express'

const app = express()

// Corpo cru: a assinatura é calculada sobre os bytes recebidos
app.post('/webhooks/cpfhub', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('x-cpfhub-signature') ?? ''
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  const expected = crypto
    .createHmac('sha256', process.env.CPFHUB_WEBHOOK_SECRET!)
    .update(`${parts.t}.${req.body}`)
    .digest()
  const received = Buffer.from(parts.v1 ?? '', 'hex')

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected) &&
    Math.abs(Date.now() / 1000 - t) < 300
  if (!valid) return res.sendStatus(401)

  const event = JSON.parse(req.body.toString())
  if (event.event === 'kyc.decision') {
    // Ligue a decisão ao seu cadastro pelo externalId
    console.log(event.externalId, event.status)
  }
  res.sendStatus(200)
})

Entrega e retentativas

  • Confirmação: responda com qualquer status 2xx em até 10 segundos. Processe o evento depois (fila), não antes de responder.
  • Retentativas: sem 2xx, por timeout ou redirecionamento, tentamos de novo em 1 min, 5 min, 30 min, 2 h e 12 h. Depois disso a entrega fica como falha, e você pode reenviar pelo painel.
  • Repetições: o mesmo evento pode chegar mais de uma vez. Ignore um x-cpfhub-delivery que você já processou.
  • Ordem: não há garantia de ordem entre eventos. Use occurredAt e, na dúvida, confirme a decisão atual com GET /kyc/links/{id}.
  • Destino: só https e só endereços públicos. Redirecionamentos não são seguidos.
⚠

Não confie no redirecionamento

A volta da pessoa para o seu redirectUrl não prova nada: ela pode abrir essa URL sozinha. Libere o cadastro só depois de um webhook com assinatura válida, ou da consulta pela API.


Atualizado em 27 de outubro de 2026