跳到正文
PayHub开发者文档

Webhook 事件与验签

平台如何推送事件、每种事件何时发送、如何验签与去重、乱序与重试、密钥轮换、测试发送与补齐遗漏事件。

最后更新于 2026-10-15 · 契约 v1.0.0

概览

  • 每个商户在每个环境(测试、生产)配置一个主端点,并选择要订阅的事件:配置 Webhook(在新标签页打开商户后台)(测试环境;需要所有者、管理员或开发者)。
  • 平台以 HTTPS POST 把事件 JSON 发送到端点,并用签名密钥签名。签名密钥与 API Key 相互独立。
  • 事件用于通知事实,不承诺推送所有内部状态:例如无需交互直接成功的支付可能没有 payment.requires_action。

投递语义

  1. 同一事件可能重复到达。
  2. 多个事件可能乱序到达。
  3. 按事件 ID 持久化去重。
  4. 按 data.object.version 处理:旧版本不能覆盖新状态,冲突时调用 API 查询最新状态。

注意

不能按状态名排序:failed 之后付款人可能换卡重试并成功,所以 payment.failed 可能晚于 payment.succeeded 到达。

事件类型

事件触发条件你通常要做什么GET /v1/events 所需权限
payment.requires_action支付进入待认证状态;无需交互而直接成功时可能不产生一般无需处理(同步响应已给出 next_action);可用于监控payments:read
payment.succeeded首次确认支付成功按 Payment ID 幂等履约payments:read
payment.failed某次尝试明确失败,载荷含失败的尝试号与当前 Payment 版本不要立即关闭订单:付款人可能换卡重试;按 version 处理payments:read
payment.canceled支付已安全取消关闭订单payments:read
payment.expired无在途尝试时到期关闭关闭订单;再次收款需新的商户订单号payments:read
payment.corrected经可信证据和受控流程纠正异常历史状态(例如本地已过期的支付被核实为真实扣款成功)查询支付最新状态并按新状态处理(如补发货,或按业务发起退款);建议人工复核这类订单payments:read
refund.pending_review退款已受理,等待审核记录refunds:read
refund.processing退款进入上游处理记录refunds:read
refund.succeeded上游确认退款成功更新订单退款状态refunds:read
refund.failed退款明确未成功通知客服;如需再次退款,用新的商户退款号申请refunds:read
refund.rejected审核驳回,未提交上游查看 failure.message,必要时重新申请refunds:read
refund.canceled提交上游前被平台取消记录refunds:read
refund.returned曾成功的退款发生真实资金退回查看 returned_amount;不要自动再退一次,先核实付款人是否收到退款,再按新的退款申请处理refunds:read
dispute.created争议案件建立在商户后台争议页面准备并上传材料,留意证据截止时间payments:read
dispute.updated案件信息或状态变化查看变化payments:read
dispute.closed结案资金影响以结算、账务流水中的真实资金事件为准payments:read
settlement.created结算单生成并占用资金对账finance:read
settlement.paid结算打款成功对账,核对银行入账finance:read
settlement.failed结算打款明确失败平台会重新生成并审批新的结算单,关注后续 settlement.createdfinance:read
settlement.returned确认银行退汇核对收款账户信息;平台不会自动用旧账户重新打款finance:read

平台将来可能新增事件类型;收到不认识的类型时返回 2xx 并忽略。

  • 平台异常退款(平台向持卡人原路退还重复扣款或金额不符多扣的款项)不发送 refund.* 事件,也不出现在退款接口中;平台另以站内信和邮件通知你,这类退还不影响你的余额与可退金额。
  • 开放 API 不提供争议资源:争议只能通过 dispute.* 事件、GET /v1/events/{event_id}(需要 payments:read)与商户后台查看。dispute.* 事件的 data.object 是争议快照:
名称类型必填说明
id
string必填

争议号(dsp_ 前缀)。

示例:dsp_demo_001

payment_id
string必填

关联的支付号。

示例:pay_demo_001

merchant_order_no
string必填

关联的商户订单号。

示例:ORDER-DEMO-001

reason
string必填

争议原因(平台原因分类):fraudulent、product_not_received、product_unacceptable、duplicate、credit_not_processed、general; 接收方须容忍新增取值。

示例:fraudulent

amount
integer(int64)必填

争议金额,最小货币单位。

约束:≥ 1

示例:10000

currency
string必填

大写三字母币种代码。

约束:格式 ^[A-Z]{3}$

示例:USD

status
string必填

案件状态(04 §5.1)。

  • needs_response
  • under_review
  • won
  • lost
  • closed
  • 将来可能新增取值;不认识的状态不要当作成功。

示例:needs_response

funds_impact
string必填

资金影响(04 §5.2)。

  • none
  • held
  • debited
  • returned
  • partially_returned
  • 将来可能新增取值;不认识的状态不要当作成功。

示例:held

evidence_due_by
string(date-time)可为空必填

证据截止时间,可为空。

示例:2026-09-30T00:00:00Z

created_at
string(date-time)必填

建案时间。

示例:2026-09-23T09:05:00Z

closed_at
string(date-time)可为空必填

结案时间;未结案为 null。

示例:null

version
integer(int64)必填

每次有效变更递增,用于丢弃旧事件。

约束:≥ 1

示例:1

事件结构

payment.succeeded 事件
{
  "id": "evt_demo_001",
  "type": "payment.succeeded",
  "api_version": "v1",
  "created_at": "2026-09-23T09:05:00Z",
  "environment": "test",
  "merchant_id": "mer_demo_001",
  "data": {
    "object": {
      "id": "pay_demo_001",
      "merchant_order_no": "ORDER-DEMO-001",
      "amount": 10000,
      "currency": "USD",
      "status": "succeeded",
      "latest_attempt_id": "pat_demo_001",
      "version": 4
    }
  }
}
名称类型必填说明
id
string必填

事件 ID,与 X-Pay-Event-Id 相同。

示例:evt_demo_001

type
string必填

事件类型。事件用于通知事实,不承诺逐一推送所有内部状态;接收方须容忍未来新增的类型。

  • payment.requires_action:支付进入待认证状态;无需交互而直接成功时可能不产生
  • payment.succeeded:首次确认支付成功
  • payment.failed:某次尝试明确失败,载荷含失败的尝试号与当前 Payment 版本
  • payment.canceled:支付已安全取消
  • payment.expired:无在途尝试时到期关闭
  • payment.corrected:经可信证据和受控流程纠正异常历史状态(例如本地已过期的支付被核实为真实扣款成功)
  • refund.pending_review:退款已受理,等待审核
  • refund.processing:退款进入上游处理
  • refund.succeeded:上游确认退款成功
  • refund.failed:退款明确未成功
  • refund.rejected:审核驳回,未提交上游
  • refund.canceled:提交上游前被平台取消
  • refund.returned:曾成功的退款发生真实资金退回
  • dispute.created:争议案件建立
  • dispute.updated:案件信息或状态变化
  • dispute.closed:结案
  • settlement.created:结算单生成并占用资金
  • settlement.paid:结算打款成功
  • settlement.failed:结算打款明确失败
  • settlement.returned:确认银行退汇
  • payment.requires_action:支付进入待认证状态;无需交互而直接成功时可能不产生
  • payment.succeeded:首次确认支付成功
  • payment.failed:某次尝试明确失败,载荷含失败的尝试号与当前 Payment 版本
  • payment.canceled:支付已安全取消
  • payment.expired:无在途尝试时到期关闭
  • payment.corrected:经可信证据和受控流程纠正异常历史状态(例如本地已过期的支付被核实为真实扣款成功)
  • refund.pending_review:退款已受理,等待审核
  • refund.processing:退款进入上游处理
  • refund.succeeded:上游确认退款成功
  • refund.failed:退款明确未成功
  • refund.rejected:审核驳回,未提交上游
  • refund.canceled:提交上游前被平台取消
  • refund.returned:曾成功的退款发生真实资金退回
  • dispute.created:争议案件建立
  • dispute.updated:案件信息或状态变化
  • dispute.closed:结案
  • settlement.created:结算单生成并占用资金
  • settlement.paid:结算打款成功
  • settlement.failed:结算打款明确失败
  • settlement.returned:确认银行退汇
  • 将来可能新增取值;不认识的状态不要当作成功。

示例:payment.succeeded

api_version
string必填

载荷的 API 版本。

示例:v1

created_at
string(date-time)必填

事件创建时间(业务时间),不能代替签名时间。

示例:2026-09-23T09:05:00Z

environment
string必填

环境:test 为测试,live 为生产。接收方须校验环境。

  • test
  • live
  • 将来可能新增取值;不认识的状态不要当作成功。

示例:test

merchant_id
string必填

商户号。

示例:mer_demo_001

data
EventData必填

事件载荷。

object
object必填

事件发生时的业务对象快照(Payment、Refund、Dispute 或 Settlement),字段与商户 API 中对应资源一致,至少包含 id 与 version。

示例:{"id":"pay_demo_001","merchant_order_no":"ORDER-DEMO-001","amount":10000,"currency":"USD","status":"succeeded","latest_attempt_id":"pat_demo_001","version":4}

  • 事件 ID 与请求头 X-Pay-Event-Id 相同,是去重键。
  • created_at 是业务时间,不能代替签名时间判断时效。
  • 接收方必须校验 environment 与 merchant_id 属于自己。
  • 同一事件重试时,事件 ID 与请求体字节不变。事件只含最小必要数据,不含完整卡号、CVC、密钥或认证秘密。
  • data.object 中的金额字段(如 amount、returned_amount)都是最小货币单位整数,并与 currency 一起出现。

警告:金额是最小货币单位整数

USD 10000 表示 100.00 美元,JPY 10000 表示 10,000 日元,不能统一除以 100;不同币种的金额不能相加。详见数据格式。

请求头

请求头示例说明
X-Pay-Event-Idevt_demo_001

事件 ID,与载荷中的 id 相同;接收方据此持久化去重。

X-Pay-Delivery-Iddel_demo_001

本次投递 ID;同一事件的每次投递都不同。

X-Pay-Test-Eventtrue

只在商户后台"发送测试事件"产生的投递中出现,值固定为 true。测试事件的对象 ID 为前缀加 26 个 0, 不对应任何真实对象;接收方不得据此履约,可据此跳过业务处理但仍应验签并返回 2xx。

X-Pay-Signaturet=1790154300,v1=0867b244eee91cdcd27eb83756e1f133889a7faec94648c80d1a0298474e3b77

签名时间与 HMAC-SHA256 签名;密钥轮换重叠期内可含多个 v1。

X-Pay-Delivery-Id 每次投递都不同,只用于排查,不要用它去重。收到 X-Pay-Test-Event: true 时只记录、不履约。

签名协议

文本
signed_payload = ASCII(t) + "." + 原始 HTTP 请求体字节
v1 = hex(HMAC-SHA256(webhook_secret, signed_payload))   // 小写十六进制,64 个字符
  1. t 是本次投递的签名时间(UNIX 秒),每次重试都重新生成;所以旧事件的合法重发不会因业务时间较早被拒。
  2. 校验 |当前时间 − t| ≤ 300 秒,双方服务器需保持时间同步(NTP)。
  3. 用常量时间比较签名(Node.js crypto.timingSafeEqual、Python hmac.compare_digest、Go hmac.Equal、Java MessageDigest.isEqual)。
  4. 头中有多个 v1 时任一匹配即通过(密钥轮换重叠期)。
  5. 必须用原始请求体字节计算:先验签、再解析 JSON;不能先解析再重新序列化。
  6. HMAC 的密钥是签名密钥的完整字符串(含 whsec_ 前缀)的 UTF-8 字节,不做 Base64 解码,也不要去掉前缀。

验签步骤与示例代码

  1. 读取原始请求体字节(对 Webhook 路由关闭框架的自动 JSON 解析,或保留 raw body)。
  2. 解析 X-Pay-Signature,取出 t 与全部 v1;格式不合法直接拒绝。
  3. 校验 t 与本地时间相差不超过 300 秒,防重放。
  4. 用当前有效的签名密钥(轮换期为新旧两个)计算期望签名。
  5. 与每个 v1 常量时间比较,任一匹配即通过;全部不匹配返回 400,不执行任何业务。
  6. 解析 JSON。
  7. 校验 environment 与 merchant_id 属于你(测试端点只接受 test)。
  8. 以事件 ID 持久化接收记录(如 INSERT … ON CONFLICT (event_id) DO NOTHING),已存在说明是重复投递。
  9. 持久化成功后尽快返回 2xx(5 秒内);持久化失败返回 5xx,让平台稍后重试。
  10. 在异步任务中处理业务:比较 data.object.version,需要时调用 API 查询最新状态。

验签函数:

Node.js
import crypto from 'node:crypto';

const TOLERANCE_SECONDS = 300;

// rawBody: Buffer(原始请求体字节);secrets: 当前有效的签名密钥数组(轮换期放新旧两个)
// 每个 secret 是完整的签名密钥字符串(含 whsec_ 前缀),按 UTF-8 字节作为 HMAC 密钥,不做 Base64 解码
export function verifySignature(
  rawBody,
  header,
  secrets,
  nowSeconds = Math.floor(Date.now() / 1000)
) {
  if (typeof header !== 'string' || !Buffer.isBuffer(rawBody)) return false;
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(',')) {
    const i = part.indexOf('=');
    if (i <= 0) continue;
    const key = part.slice(0, i).trim();
    const value = part.slice(i + 1).trim();
    if (key === 't') timestamp = value;
    else if (key === 'v1') signatures.push(value);
  }
  if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`, 'ascii'), rawBody]);
  for (const secret of secrets) {
    const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest();
    for (const sig of signatures) {
      if (!/^[0-9a-f]{64}$/.test(sig)) continue; // 长度不等时 timingSafeEqual 会抛错,先校验格式
      if (crypto.timingSafeEqual(expected, Buffer.from(sig, 'hex'))) return true;
    }
  }
  return false;
}

接收端:

Node.js(Express)
import express from 'express';
import { verifySignature } from './verify.mjs';

const app = express();

// 只对 Webhook 路由使用 raw 解析,拿到原始字节;不要在它之前全局挂 express.json()
app.post(
  '/webhooks/payhub',
  express.raw({ type: 'application/json', limit: '1mb' }),
  async (req, res) => {
    const secrets = [
      process.env.PAYHUB_WEBHOOK_SECRET,
      process.env.PAYHUB_WEBHOOK_SECRET_PREVIOUS
    ].filter(Boolean);
    if (!verifySignature(req.body, req.get('X-Pay-Signature'), secrets)) {
      return res.status(400).send('invalid signature'); // 不执行任何业务
    }
    const event = JSON.parse(req.body.toString('utf8'));
    if (
      event.environment !== process.env.PAYHUB_ENVIRONMENT ||
      event.merchant_id !== process.env.PAYHUB_MERCHANT_ID
    ) {
      return res.status(400).send('unexpected environment or merchant');
    }
    if (req.get('X-Pay-Test-Event') === 'true') {
      await recordTestEvent(event.id); // 测试事件只记录、不履约
      return res.status(200).end();
    }
    let isNew;
    try {
      isNew = await saveEventIfAbsent(event.id, req.body); // INSERT ... ON CONFLICT (event_id) DO NOTHING
    } catch {
      return res.status(500).end(); // 没能持久化:返回非 2xx,让平台稍后重试
    }
    res.status(200).end(); // 持久化后尽快应答
    if (isNew) enqueue(event.id).catch(() => {}); // 入队失败由定时任务扫描"已接收未处理"的事件补偿
  }
);

app.listen(Number(process.env.PORT ?? 8080));

其他语言的实现须满足同样的要求:在解码 JSON 前读取完整原始字节;用常量时间比较;支持多个 v1 与 300 秒容差;并通过下面的测试向量。

测试向量

用下面的输入验证你的验签实现(数值由文档构建时计算):

演示密钥(仅用于验证实现,不是任何环境的真实密钥;带 whsec_ 前缀)whsec_demo_webhook_secret_for_docs_only
t1790154300(2026-09-23T09:05:00Z)
原始请求体(330 字节,单行、无多余空格、无结尾换行){"id":"evt_demo_001","type":"payment.succeeded","api_version":"v1","created_at":"2026-09-23T09:05:00Z","environment":"test","merchant_id":"mer_demo_001","data":{"object":{"id":"pay_demo_001","merchant_order_no":"ORDER-DEMO-001","amount":10000,"currency":"USD","status":"succeeded","latest_attempt_id":"pat_demo_001","version":4}}}
期望 v10867b244eee91cdcd27eb83756e1f133889a7faec94648c80d1a0298474e3b77
轮换演示:旧密钥 whsec_demo_webhook_secret_previous_docs_only 的签名44a4429c71945a5c66e80b0a64196b84cade4adeafb454a11839c4afa34fb9f2
去掉 whsec_ 前缀计算得到(与期望不同,验签必须失败)825c83c31826be26c5e4b84f0e9f21c08b83acec8ac616a852531dd8b70a6bab

你的实现应得到以下结果(“当前时间”固定在 1790154300 附近):

  • 正确签名:通过
  • 轮换期两个 v1,只配置新密钥:通过
  • 改动请求体(amount 10000 → 1000):失败
  • 请求体重新格式化为多行 JSON:失败
  • 去掉 whsec_ 前缀计算:失败
  • 超过 300 秒容差:失败
下载测试向量 JSON

本地命令自查:

openssl
# body.json 保存测试向量的原始请求体(不要带结尾换行)
{ printf '%s.' "1790154300"; cat body.json; } | openssl dgst -sha256 -hmac "whsec_demo_webhook_secret_for_docs_only"
# 输出应以 0867b244eee91cdcd27eb83756e1f133889a7faec94648c80d1a0298474e3b77 结尾

注意

验证测试向量时要把“当前时间”固定在 t 附近(示例函数的 nowSeconds / now 参数),否则会因超过 300 秒容差而失败。

接收方最佳实践

  1. 顺序固定:验签 → 校验环境与商户号 → 按事件 ID 持久化去重 → 尽快 2xx → 异步处理业务。
  2. 业务处理必须幂等:即使去重了,你的 2xx 也可能没送达平台,平台会重投同一事件。
  3. 不依赖事件顺序:比较 version,冲突或缺失时调用 API 查询最新状态。
  4. 不认识的事件类型返回 2xx 并记录,不要返回错误(否则平台会持续重试 48 小时)。
  5. 5 秒内应答;耗时的工作放进队列。
  6. 不要返回 3xx:http → https 跳转、补尾斜杠跳转、登录跳转都会被视为失败。
  7. Webhook 路由跳过登录与 CSRF 校验,凭签名鉴权;WAF 放行该路由。
  8. 测试环境与生产环境使用不同的端点与签名密钥,代码中按环境分别配置。
  9. 日志可以记录事件 ID、投递 ID、事件类型;不要记录签名密钥。
  10. 监控:在商户后台查看投递记录(事件投递记录(在新标签页打开商户后台)(需要所有者、管理员或开发者));投递最终失败时技术联系人与开发者会收到每小时汇总邮件。

乱序与版本比较

业务发生顺序事件data.object.version实际到达顺序正确处理
1payment.failed(尝试 1 被发卡行拒绝)3第 2 个到达本地已是 v5,3 < 5,忽略
2付款人换卡重试,尝试 2 成功:payment.succeeded5第 1 个到达本地无记录或版本更低,应用并履约
Node.js:按 version 处理事件
// 按 version 处理乱序事件(伪代码,按你的数据库改写)
export async function applyPaymentEvent(db, event) {
  const incoming = event.data.object;
  await db.transaction(async (tx) => {
    const local = await tx.findPaymentForUpdate(incoming.id); // 锁住本地记录
    if (local && local.version >= incoming.version) return; // 重复或旧事件:忽略
    await tx.savePayment({ id: incoming.id, status: incoming.status, version: incoming.version });
    if (incoming.status === 'succeeded') await tx.enqueueFulfillment(incoming.id); // 履约按 Payment ID 幂等
  });
}

异步任务取到事件后的处理顺序:

事件处理决策
事件处理决策

重试计划

第几次时点(距事件首次排队)
1立即
21 分钟
35 分钟
415 分钟
51 小时
66 小时
724 小时
848 小时
  • 成功条件:单次 5 秒内收到 HTTP 2xx;超时视为本次投递失败,不代表业务失败。
  • 任何非 2xx 都视为本次投递失败并按计划重试,包括 4xx:例如你的 WAF 返回 401 / 403,平台也会持续重试。
  • 不跟随 3xx:返回重定向视为失败。
  • 共 8 次(含首次)全部失败:进入失败投递列表并告警;之后可由平台人工重发——复用事件 ID,生成新的投递 ID 与签名,不会重做支付或记账。
  • 每次投递的 X-Pay-Delivery-Id、t、签名都不同;事件 ID 与请求体不变。
投递、验签与重试
投递、验签与重试

投递记录中的汇总状态(事件投递记录(在新标签页打开商户后台)(需要所有者、管理员或开发者)):

投递汇总状态
投递汇总状态
  • 事件在业务事务内写入:状态已经提交时,即使平台服务随后崩溃,通知也会继续发送。所以“支付成功了却没收到通知”时,先查投递记录,再按补齐遗漏事件处理。
  • 同一业务事实只有一个事件 ID;多次投递不会产生多次业务效果。

端点地址要求

要求说明
HTTPS必须;证书须由公共 CA 签发且在有效期内(自签名或过期证书按投递失败处理)
公网可达不能指向内网、环回、链路本地、云元数据或其他受限地址;平台同时检查 IPv4、IPv6、DNS 解析结果与实际连接地址,防止 DNS 重绑定,配置或实际发送时被拦截
端口只能是默认的 443,或 1024–65535;其他端口在配置时被拒绝
不重定向发送器不跟随 3xx
不支持自定义代理与秘密请求头来源真实性只靠签名验证
每环境一个主端点测试、生产各一个;在 Webhook 配置(在新标签页打开商户后台)(需要所有者、管理员或开发者) 中设置并选择订阅的事件
修改端点需要二次认证并留审计;变更会通知所有者、管理员与技术联系人

签名密钥与轮换

  1. 签名密钥只在 Webhook 配置(在新标签页打开商户后台)(需要所有者、管理员或开发者) 中创建端点与轮换时一次性展示,之后不能再查看;请立即存入你的密钥管理服务,遗失时只能轮换。密钥与 API Key 无关,轮换 API Key 不影响 Webhook。
  2. 发起轮换(需二次认证)后,新密钥只显示一次,同时进入重叠期:平台同时用新旧密钥签名,X-Pay-Signature 带两个 v1。
  3. 把新密钥加入你的配置(示例中的 secrets 数组同时放新旧两个)并部署。
  4. 重叠期结束后旧密钥停用,只剩新签名;从配置中删除旧密钥。
  5. 重叠期最长 24 小时(默认 24 小时),发起轮换时可选择更早停用旧密钥,重叠期内也可以立即停用;怀疑泄漏时立即轮换并尽快停用旧密钥。

测试发送与本地调试

  • 在商户后台选择事件类型“发送测试事件”到当前环境的端点(Webhook 配置(在新标签页打开商户后台)(测试环境;需要所有者、管理员或开发者));测试发送不会产生真实支付、退款或财务分录。
  • 测试事件带请求头 X-Pay-Test-Event: true,用当前签名密钥正常签名(重叠期内同样双签);只投递 1 次,不进入自动重试,在投递记录中带“测试”标签。
  • 测试事件的对象 ID 不对应平台中的真实对象,查询会得到 404;收到带 X-Pay-Test-Event: true 的事件时只记录、不履约。
  • 本地开发:平台不会访问内网或本机地址;通过公开测试端点或受控隧道把本地服务暴露为公网 HTTPS 地址。
  • 发送后在 事件投递记录(在新标签页打开商户后台)(测试环境;需要所有者、管理员或开发者) 查看结果(HTTP 状态、耗时、错误摘要)。完整测试步骤见测试指南。

补齐遗漏事件

  • 在 事件投递记录(在新标签页打开商户后台)(需要所有者、管理员或开发者) 中找到失败事件的 ID,调用 GET/v1/events/{event_id} 获取载荷(与投递时一致,事件 ID 不变)。所需权限按事件所属业务;事件不会自动清除,历史事件都可查询;不属于当前商户或环境的事件返回 404。
  • 或直接查询对象最新状态(如 GET /v1/payments/{payment_id},或按 status、created_gte、created_lt 列表查询)完成对账。
  • 需要重新投递时联系平台技术支持(人工重发复用事件 ID)。

故障排查

现象可能原因解决
签名总是不匹配框架先解析 JSON 再序列化;用错密钥(测试 / 生产混用、轮换后仍用旧密钥);网关或中间件改写了请求体(解压、字符集转换、去空格);把 t 当毫秒读取原始字节;核对环境与密钥;接入层不改写请求体;t 是秒
提示时间戳超出容差服务器时钟不同步启用 NTP
投递记录显示返回 3xxhttp → https 跳转、尾斜杠跳转、登录跳转配置最终地址;Webhook 路由不做跳转
投递记录显示超时在请求中同步处理业务先持久化再返回 2xx,业务异步
投递记录显示地址被拦截指向内网或受限地址;域名解析到私有地址使用公网地址
同一事件被处理两次没有按事件 ID 去重;你的 2xx 未送达平台导致重投按事件 ID 持久化去重,业务幂等
订单状态被旧事件覆盖按到达顺序处理比较 version
你的 WAF 返回 401 / 403WAF 拦截了平台请求放行 Webhook 路由,凭签名鉴权
这页有帮助吗?