身份验证 Webhook
身份验证的每一个审核结果,无论是自动作出还是由您的团队在控制台中作出,都会向您登记的 URL 发送一个 POST。事件 不含个人数据,只包含 ID、审核结果和原因。
配置
在控制台的 Verificações > Configurações > Integrações(验证 > 设置 > 集成)中:
- 登记您的端点 URL。仅支持
https和公网地址。 - 复制 签名密钥,保存在环境变量中(例如
CPFHUB_WEBHOOK_SECRET)。 - 使用测试推送接收一个
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"
}| 字段 | 类型 | 说明 |
|---|---|---|
event | string | kyc.decision,测试推送时为 kyc.test |
eventId | string | 事件 ID |
verificationId | string | 验证 ID |
linkId | string | null | POST /kyc/links 返回的 id |
externalId | string | null | 您创建链接时发送的标识 |
status | string | 审核结果:approved、manual_review 或 rejected。见审核结果。 |
previousStatus | string | null | 审核结果发生变化时的上一个结果(例如从 manual_review 变为 approved) |
decidedBy | string | automatic(账户规则)或 manual(您的团队在控制台中作出) |
reasons | array | 自动审核的原因:[{ "code", "message" }]。人工审核时为空。 |
occurredAt | string | 作出审核结果的时间(ISO 8601,UTC) |
请求头
| 请求头 | 说明 |
|---|---|
x-cpfhub-event | 与 event 的值相同 |
x-cpfhub-delivery | 推送 ID。用于忽略重复推送。 |
x-cpfhub-signature | t=<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 日