CPFHub.io

身份验证 Webhook

身份验证的每一个审核结果,无论是自动作出还是由您的团队在控制台中作出,都会向您登记的 URL 发送一个 POST。事件 不含个人数据,只包含 ID、审核结果和原因。

配置

在控制台的 Verificações > Configurações > Integrações(验证 > 设置 > 集成)中:

  1. 登记您的端点 URL。仅支持 https 和公网地址。
  2. 复制 签名密钥,保存在环境变量中(例如 CPFHUB_WEBHOOK_SECRET)。
  3. 使用测试推送接收一个 kyc.test 事件,并校验签名。

事件

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"
}
字段类型说明
eventstringkyc.decision,测试推送时为 kyc.test
eventIdstring事件 ID
verificationIdstring验证 ID
linkIdstring | nullPOST /kyc/links 返回的 id
externalIdstring | null您创建链接时发送的标识
statusstring审核结果:approved、manual_review 或 rejected。见审核结果。
previousStatusstring | null审核结果发生变化时的上一个结果(例如从 manual_review 变为 approved)
decidedBystringautomatic(账户规则)或 manual(您的团队在控制台中作出)
reasonsarray自动审核的原因:[{ "code", "message" }]。人工审核时为空。
occurredAtstring作出审核结果的时间(ISO 8601,UTC)

请求头

请求头说明
x-cpfhub-event与 event 的值相同
x-cpfhub-delivery推送 ID。用于忽略重复推送。
x-cpfhub-signaturet=<unix>,v1=<hex>:发送时间(秒)和签名

校验签名

v1 是用您的签名密钥对 "<t>.<raw body>" 计算的 HMAC-SHA256,以十六进制表示。请基于 原样接收的请求体 计算,不要先做 JSON 解析。使用常量时间比较,并拒绝 t 与您的时钟相差超过 5 分钟的事件。

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

const app = express()

// Raw body: the signature is computed over the received bytes
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') {
    // Tie the decision to your sign-up through the externalId
    console.log(event.externalId, event.status)
  }
  res.sendStatus(200)
})

推送与重试

  • 确认接收: 请在 10 秒内返回任意 2xx 状态码。先响应,再处理事件(放入队列)。
  • 重试: 未收到 2xx、超时或遇到重定向时,我们会在 1 分钟、5 分钟、30 分钟、2 小时和 12 小时后重试。之后该推送会被标记为失败,您可以在控制台中重新发送。
  • 重复: 同一事件可能送达多次。请忽略已处理过的 x-cpfhub-delivery。
  • 顺序: 事件之间不保证顺序。请参考 occurredAt,如有疑问,请用 GET /kyc/links/{id} 确认当前审核结果。
  • 目标地址: 仅支持 https 和公网地址。不跟随重定向。
⚠

不要依赖重定向

用户回到您的 redirectUrl 并不能证明任何事情,因为用户可以自行打开该 URL。只有在收到签名有效的 webhook,或通过 API 查询确认后,才可以批准注册。


更新于 2026 年 10 月 27 日