
Webhook 是您的商店、计费面板或机器人得知客户已付款的方式。每当发生账单、付款或提现事件时,SnapEX Pay 都会向您的 URL 发送 HTTP POST。处理得当,它是您最快、最可靠的信号;处理草率,它就成了把从未付款的订单标记为已支付的漏洞。本指南介绍其中的关键部分。
设置
在商户后台进入 Invoices → Webhook 卡片,输入您的 URL 并点击 Save。每个账户一个 URL,接收所有类型的事件。首次保存时,后台只显示一次签名密钥 whsec_…:请只保存在您的服务器上。
URL 必须是带公网主机的绝对 https:// 地址:不能包含 login:password,不能是 localhost,主机也不能解析到私有或保留 IP。不会跟随重定向,因此请提供最终 URL。
收到的内容
每个请求带有四个请求头:
| 请求头 | 含义 |
|---|---|
Webhook-Id |
唯一的事件 ID。重试或手动重新发送时不会改变。 |
Webhook-Timestamp |
本次尝试发送时的 Unix 时间(秒)。每次重试都是新值。 |
Webhook-Signature |
v1= 加上 {Webhook-Timestamp}.{raw body} 的小写十六进制 HMAC-SHA256。 |
Content-Type |
始终为 application/json。 |
请求体包含 id(与 Webhook-Id 相同)、type、created_at 和 data。对于 invoice.* 事件,data 是与 GET /invoices/{id} 结构相同的账单,包括 environment、amount_usd、status 和您的 external_id。
账单事件:
| 事件 | 何时发送 |
|---|---|
invoice.created |
账单已创建,尚未选择支付方式。 |
invoice.awaiting |
已选择支付方式并分配了地址。 |
invoice.underpaid |
部分金额已到账;同一地址等待剩余金额。 |
invoice.paid |
全额已支付并入账。这是发货的唯一信号。 |
invoice.partially_paid |
只支付了一部分,补款时间已结束,已收到的部分记入您的余额。 |
invoice.expired |
账单时限结束且未付款。 |
invoice.extra_payment |
账单付清后又到达一笔转账;单独入账。 |
校验签名
- 在任何 JSON 解析之前,以字节形式读取原始请求体。重新序列化的 JSON 不会匹配。
- 以完整密钥(包含
whsec_前缀)为键,对{Webhook-Timestamp}.{raw body}计算 HMAC-SHA256,并进行十六进制编码。 - 使用常量时间函数,将
v1=+ 十六进制值与Webhook-Signature请求头进行比较。 - 拒绝与您的时钟相差超过 5 分钟的
Webhook-Timestamp。重试带有新的时间戳,因此不会因此失败。
PHP:
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… from the cabinet
$body = file_get_contents('php://input'); // raw body, before json_decode
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// skip if $event['id'] was already processed; do the heavy work in a queue
http_response_code(200);
Node.js(Express):
const crypto = require('crypto');
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('Webhook-Timestamp') || '';
const sig = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto
.createHmac('sha256', process.env.SNAPEX_WEBHOOK_SECRET)
.update(ts + '.' + req.body)
.digest('hex'));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// skip if event.id was already processed; queue the heavy work
res.sendStatus(200);
});
重试、重复投递与顺序
- 任何 2xx 都算成功。 其他状态码、超时或网络错误都算失败。连接超时 5 秒,整个请求超时 15 秒,因此请快速响应,并异步执行耗时工作。
- 重试。 每个事件最多尝试 5 次:首次发送,之后每次失败后约 10 秒、1 分钟、5 分钟和 15 分钟重试。第五次失败后停止自动投递;后台会列出最近的投递,并提供 Retry 按钮,以相同的
Webhook-Id重新发送事件。 - 重复投递。 投递方式为至少一次(at-least-once):同一事件可能到达不止一次。保存已处理的
Webhook-Id,忽略重复的。 - 顺序。 事件可能乱序到达:一个被重试的旧事件可能在较新的事件之后到达。不要依赖顺序;将事件中的状态与您已有的状态进行比较。
- 如需可再次确认。 收到 Webhook 后,您可以通过 API 重新读取对象:
GET /invoices/{id}。
不用真实资金测试
创建一张测试账单("environment": "test"),在支付页面付款,观察带有 data.environment = "test" 的 Webhook 到达。详情:如何不用真实资金测试加密货币支付。
适用于 WHMCS、BILLmanager、WooCommerce 及其他平台的现成模块已经会校验签名,且绝不会重复入账;参见 pay.snapex.pro。完整参考:SnapEX Pay API 文档。