Webhook 与签名
支付与退款事件会以 HTTPS POST 推送到商户后台配置的回调地址。每个请求都带 HMAC-SHA256 签名,失败会自动重试,也可在后台手动重投。
配置回调地址
- 打开商户后台 →「Webhook」。
- 填写 HTTPS 地址,例如
https://shop.example/webhooks/spacepay。不能指向本机或内网地址。 - 保存后,同一页面可以查看签名密钥(可重置)与最近 100 条投递记录。
i你的服务端应在 10 秒内返回 2xx。返回非 2xx、超时或连接失败都会触发重试。
事件类型
| 事件 | 触发时机 |
|---|---|
payment.succeeded | 付款人完成支付,资金进入商户待结算余额 |
payment.refunded | 商户或运营发起退款后 |
事件结构
{
"id": "evt_9f8e7d6c5b4a3210",
"type": "payment.succeeded",
"created": 1762800000,
"data": {
"id": "pay_1",
"numeric_id": 1,
"reference": "order-2026-0001",
"amount_cents": 1234,
"amount": "12.34",
"currency": "USD",
"status": "SUCCEEDED",
"refunded_cents": 0,
"refundable_cents": 1234,
"description": "拿铁两杯",
"customer_email": "[email protected]",
"metadata": { "sku": "latte" },
"created_at": 1762800000000,
"paid_at": 1762800030000
}
}
data 是事件发生时的支付单快照。退款事件的 status 为 PARTIALLY_REFUNDED 或 REFUNDED,refunded_cents 为累计退款金额。
请求头
| 请求头 | 说明 |
|---|---|
Content-Type | application/json |
X-SpacePay-Event | 事件类型,如 payment.succeeded |
X-SpacePay-Delivery | 事件 ID,重试时保持不变,用于去重 |
X-SpacePay-Signature | t=时间戳,v1=签名 |
User-Agent | SpacePay-Webhooks/1.0 |
校验签名
签名 = HMAC-SHA256(webhook_secret, 时间戳 + "." + 原始请求体),十六进制小写。必须使用原始字节计算,不能先解析 JSON 再重新序列化。
import crypto from 'node:crypto';
// rawBody 必须是未经解析的请求体字符串 / Buffer
function verifyEvent(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=')),
);
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib
import hmac
def verify_event(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# 在商户后台「Webhook」页重置签名密钥后,会显示一次新的 secret。 # 本地复算签名(把 $SECRET / $TS / $BODY 换成实际值): printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"
✓建议同时校验时间戳:与当前时间相差超过 5 分钟的事件视为过期,丢弃并记录日志,降低重放风险。
重试与去重
- 首次投递失败后,按 1 分钟、5 分钟、30 分钟、2 小时、6 小时 重试,共 6 次。
- 同一事件的
X-SpacePay-Delivery在所有重试中保持不变,请用它做幂等去重。 - 最终失败后,投递记录保留在商户后台,可手动「重投」。
- 请始终以事件处理结果为准;Webhook 可能乱序,处理时比较事件时间或支付状态。
最佳实践
- 先验签、后解析,并且只接受 2xx 确认。
- 快速返回:先把事件写入队列或数据库,再异步处理业务,避免 10 秒超时。
- 按事件 ID 去重,处理过的 ID 保留至少 24 小时。
- 不要只依赖 Webhook:定期用查询接口补偿丢失的事件。
- 密钥轮换:在商户后台重置签名密钥后,服务端需立即更新;重置会使旧密钥失效。
!Webhook 地址由商户配置,SpacePay 会在投递前解析域名并拒绝内网地址,避免被当作访问内网的跳板。