实现
完整示例
以下示例接收交易事件、验证 X-Avista-Signature 签名、快速返回 2xx 并避免重复处理。密钥生成和签名细节请参见 HMAC 签名。
HMAC 签名 (X-Avista-Signature)
在面板中生成签名密钥后,发送到您账户的每个 webhook 都会包含 X-Avista-Signature header,其值为使用该密钥计算的原始请求体的 HMAC-SHA256(十六进制)。验证签名可以确保 webhook 来自 Gateways(而不是发现了您 URL 的第三方),并且 payload 在传输过程中未被篡改。
该 header 的名称确实就是 X-Avista-Signature。这是目前发送的技术名称;请在验证程序中原样使用(PHP 中为 $_SERVER['HTTP_X_AVISTA_SIGNATURE'])。
密钥是按账户的:同一个密钥为所有已注册 URL 上的所有事件(交易和 MED)签名。手动重发同样使用账户的当前密钥签名。
在生成密钥之前,webhook 投递时不带 X-Avista-Signature header。如果您是从零开始集成,请在验证程序上线前生成密钥,并拒绝不带该 header 的请求,而不仅仅是签名错误的请求。
生成和轮换密钥
密钥只能通过面板(https://<dashboard-host>)生成和轮换;没有对应的 API 端点。
进入 设置 → Webhooks(Configurações → Webhooks),找到 HMAC 签名密钥(Chave de assinatura HMAC)卡片。
点击 轮换(Rotacionar)并确认您的密码。同一操作既用于生成第一个密钥,也用于替换现有密钥。
密钥只显示一次,显示窗口只有在您勾选已复制的确认后才能关闭。之后面板只显示密钥的结尾(最后 4 个字符)和最近一次轮换的日期。Gateways 无法找回密钥:如果丢失,请生成新的密钥。
将密钥保存在 secret manager 或环境变量中(切勿放在受版本控制的文件中):
export PIX_API_WEBHOOK_SECRET=<面板中生成的密钥>从此时起,后续的 webhook 都会带有 X-Avista-Signature header。
轮换时不拒绝传输中的 webhook
轮换会立即替换密钥:此后发送的 webhook 使用新密钥。已在队列中的 webhook 可能仍使用旧密钥签名。因此在切换期间,请在约 30 分钟内同时接受两个密钥:
const SECRETS = [
process.env.PIX_API_WEBHOOK_SECRET_CURRENT,
process.env.PIX_API_WEBHOOK_SECRET_PREVIOUS,
].filter(Boolean) as string[];
function isValid(rawBody: Buffer, received: string): boolean {
const b = Buffer.from(received, 'hex');
return SECRETS.some((secret) => {
const a = Buffer.from(
crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
'hex',
);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}此期间过后,移除 PIX_API_WEBHOOK_SECRET_PREVIOUS。在怀疑密钥泄露或进行定期轮换(例如每 90 天)时执行轮换。
如何验证
签名覆盖的正是原始请求体(rawBody,UTF-8),在任何 JSON.parse 之前。如果您重新序列化收到的对象来计算 HMAC,任何差异(键顺序、空格、编码)都会导致签名不匹配。
请始终使用时间安全的比较(crypto.timingSafeEqual / hmac.compare_digest / hmac.Equal / hash_equals)。使用 == 比较会带来时序攻击风险。
编码:请求体为 UTF-8。在需要显式字节的语言(Java/Kotlin/PHP)中,请以字节形式读取流,计算 HMAC 前不要转换为其他编码的字符串。在 Python 中,request.get_data() 已返回字节;在 Node 中,Buffer 是正确的类型。
自定义 header
URL 上注册的自定义 header(最多 5 个)会随每个 webhook 与签名一起发送。如果您已经在验证自己的 header(例如 Authorization: Basic ...),可以继续验证:X-Avista-Signature 是附加的,不会替代这些 header。
幂等性
Webhook 可能被投递多次(重试和手动重发)。请实现幂等处理以避免重复处理。
对于交易事件,请使用 event + transactionId + status 的组合作为键。这样,同一交易状态变化时会重新处理,而同一事件的重复投递会被忽略。对于 MED 事件,请使用 medId + event。
const key = `webhook:${payload.event}:${payload.transactionId}:${payload.status}`;
const isNew = await redis.set(key, '1', 'EX', 86400, 'NX');
if (!isNew) {
console.log('Webhook 已处理,忽略');
return;
}
await processWebhook(payload);最佳实践
重试
如果您的端点未在 10 秒内返回 2xx(或连接失败),投递将被重试:
| 尝试 | 间隔 | 累计时间 |
|---|---|---|
| 第 1 次 | 立即 | 0 分钟 |
| 第 2 次(第 1 次重试) | 30 秒 | 30 秒 |
| 第 3 次(第 2 次重试) | 1 分钟 | 1 分 30 秒 |
| 第 4 次(第 3 次重试) | 3 分钟 | 4 分 30 秒 |
| 第 5 次(第 4 次重试) | 5 分钟 | 9 分 30 秒 |
| 第 6 次(第 5 次重试) | 15 分钟 | 24 分 30 秒 |
6 次尝试均失败后(约 25 分钟),自动投递结束。请使用手动重发或通过 API 查询交易,以免遗漏任何事件。
当事件注册了多个 URL 时,重试只针对失败的 URL。已成功接收的 URL 不会再次收到同一事件。
响应代码
您的端点应返回合适的 HTTP 代码:
| 代码 | 描述 | 系统行为 |
|---|---|---|
2xx | 成功(200、201、204 等) | Webhook 已确认,不会重试 |
3xx | 重定向 | 不会被跟随;视为失败并重试 |
4xx | 客户端错误 | 视为失败并重试 |
5xx | 服务器错误 | 视为失败并重试 |
系统只评估 HTTP 代码。任何 2xx(200-299)响应都视为成功,与响应体无关。您可以返回空响应体、"OK" 或任意 JSON。