Webhook
资金一有动静,马上知道。
Webhook 会在事件发生时主动推送到您的服务器,无需轮询。每个事件都经过签名,在您确认收到之前会持续重试,并且可以安全地重复处理。
◷ 沙盒将先向候补名单中的开发者开放。TujuPay 获得牌照后,才会提供正式密钥。
设置接收端点
- 1
添加 URL
在商家后台打开“开发者”,再进入“Webhook”,添加您服务器上的公开 HTTPS URL。
- 2
选择事件
选择您需要的事件,或在开发阶段订阅所有事件。
- 3
复制签名密钥
每个端点都有独立的密钥,以 whsec_ 开头。请像保管密码一样妥善保存。
验证签名
每个请求都带有 TujuPay-Signature 请求头,其中包含时间戳,以及对时间戳和原始请求体计算的 HMAC-SHA256 签名。请用您的密钥计算同样的签名并进行比对。拒绝超过五分钟的事件,以防重放攻击。
请求头格式
TujuPay-Signature: t=1791536400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Node.js
import crypto from "node:crypto";
export function verifyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Date.now() / 1000 - Number(parts.t);
if (age > 300) throw new Error("Event too old");
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) throw new Error("Bad signature");
return JSON.parse(rawBody);
}PHP
<?php
function verify_webhook(string $rawBody, string $header, string $secret): array {
parse_str(str_replace(',', '&', $header), $parts);
if (time() - (int) $parts['t'] > 300) {
throw new Exception('Event too old');
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
if (!hash_equals($expected, $parts['v1'])) {
throw new Exception('Bad signature');
}
return json_decode($rawBody, true);
}重试机制
如果您的服务器未在 10 秒内返回 2xx,我们会以逐渐拉长的间隔重试,最长持续三天。您也可以在商家后台重新发送任何事件。
- 11 分钟
- 25 分钟
- 330 分钟
- 42 小时
- 56 小时
- 6之后每 12 小时一次,最长 3 天
事件
| 事件 | 发送时机 |
|---|---|
| payment.succeeded | 顾客已付款,且款项已确认 |
| payment.failed | 银行拒绝付款,或顾客放弃结账 |
| payment.expired | 付款未在 30 分钟内完成 |
| refund.succeeded | 退款已到达顾客账户 |
| refund.failed | 退款无法完成 |
| payout.scheduled | 今日结算金额已算出,即将发出 |
| payout.paid | 结算款项已转入您的银行 |
| payout.held | 结算被暂扣,事件中会说明原因 |
最佳实践
- 立即返回 200,耗时的工作(例如发送邮件)放到后台任务中处理。
- 保存每个事件 ID,跳过已处理过的事件。重试可能导致同一事件送达两次。
- 务必验证签名。切勿信任重定向 URL 中的金额或状态。
- 如需最新状态,请通过 API 查询付款;事件到达的顺序可能不一致。