Webhook 与签名

支付与退款事件会以 HTTPS POST 推送到商户后台配置的回调地址。每个请求都带 HMAC-SHA256 签名,失败会自动重试,也可在后台手动重投。

配置回调地址

  1. 打开商户后台 →「Webhook」。
  2. 填写 HTTPS 地址,例如 https://shop.example/webhooks/spacepay。不能指向本机或内网地址。
  3. 保存后,同一页面可以查看签名密钥(可重置)与最近 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-Typeapplication/json
X-SpacePay-Event事件类型,如 payment.succeeded
X-SpacePay-Delivery事件 ID,重试时保持不变,用于去重
X-SpacePay-Signaturet=时间戳,v1=签名
User-AgentSpacePay-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);
}
✓建议同时校验时间戳:与当前时间相差超过 5 分钟的事件视为过期,丢弃并记录日志,降低重放风险。

重试与去重

  • 首次投递失败后,按 1 分钟、5 分钟、30 分钟、2 小时、6 小时 重试,共 6 次。
  • 同一事件的 X-SpacePay-Delivery 在所有重试中保持不变,请用它做幂等去重。
  • 最终失败后,投递记录保留在商户后台,可手动「重投」。
  • 请始终以事件处理结果为准;Webhook 可能乱序,处理时比较事件时间或支付状态。

最佳实践

  1. 先验签、后解析,并且只接受 2xx 确认。
  2. 快速返回:先把事件写入队列或数据库,再异步处理业务,避免 10 秒超时。
  3. 按事件 ID 去重,处理过的 ID 保留至少 24 小时。
  4. 不要只依赖 Webhook:定期用查询接口补偿丢失的事件。
  5. 密钥轮换:在商户后台重置签名密钥后,服务端需立即更新;重置会使旧密钥失效。
!Webhook 地址由商户配置,SpacePay 会在投递前解析域名并拒绝内网地址,避免被当作访问内网的跳板。