Webhook 通知

在商户后台或通过 POST /api/v2/webhook-endpoints 配置接收地址后, 平台会将事件以 POST JSON 推送到你的服务器。发货必须以 Webhook(或主动查单)为准, 浏览器同步跳转不可作为发货依据。

事件结构

POST https://your-server.com/webhooks/llp
Content-Type: application/json
X-LLP-Event: payment.succeeded
X-LLP-Signature: t=1787000000,v1=6f3a...   # HMAC-SHA256(secret, "{t}.{body}")

{
  "id": "evt_cme0...",              // 事件 ID:Replay 时保持不变
  "type": "payment.succeeded",
  "created_at": "2026-08-25T12:00:05.000Z",
  "merchant_id": "100001",          // 商户号
  "api_version": "2026-08-01",
  "data": {
    "payment_intent_id": "cmt7...",
    "system_order_no": "P2026...",
    "merchant_order_no": "M2026...",
    "amount": "100.00",
    "fee": "5.00",
    "currency": "CNY",
    "status": "SUCCEEDED",
    "subject": "VIP 会员",
    "paid_at": "2026-08-25T12:00:04.000Z"
  }
}

事件类型

类型时机
payment.created / payment.processing订单创建 / 出码成功
payment.succeeded可信 Provider 事件确认支付成功(发货依据)
payment.failed / payment.expired / payment.canceled终态
refund.processing / refund.succeeded / refund.failed退款生命周期
reconciliation.discrepancy对账差异(预留)

验签(Node.js)

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=')),
  );
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  // 可选:校验 parts.t 与当前时间差 < 5 分钟,防重放
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

app.post('/webhooks/llp', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(req.body.toString(), req.headers['x-llp-signature'], WEBHOOK_SECRET)) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // 幂等:以 event.id 去重(Replay 会复用同一 event.id 且必然重复投递)
  if (!alreadyProcessed(event.id) && event.type === 'payment.succeeded') {
    ship(event.data.merchant_order_no);
  }
  res.status(200).send('ok'); // 2xx 视为投递成功
});

重试与死信

  • 非 2xx 或超时(10s)视为失败,按指数退避最多投递 12 次(约 5.7 小时窗口)
  • 重试耗尽进入死信(DEAD),可在商户后台「Webhook」或平台后台手动 Replay
  • Replay 生成新的 delivery,但 event.id 保持不变 —— 请务必按 event.id 做幂等
  • 处理逻辑应快速返回(先落库再异步处理),避免超时误判
安全提示:回调地址必须为公网可达的 https 地址(生产环境拒绝内网/本机地址); 验签失败的请求直接丢弃,不要输出任何差异化错误信息。