Paddle 如何投递 Webhook

Paddle 在生产环境下约 2 天内最多尝试投递 60 次,第一个小时约 10 次。附 ts/h1 签名格式和 Node 验证器。

投递行为

取自该平台的官方文档,最近核对于 2026-08-06。平台没有给出数字的地方,这张表也不填。

响应时限5 秒

检测你的处理程序能否及时应答

重试生产环境:约 2 天内最多尝试 60 次,第一个小时内约 10 次,第一天内约 45 次。沙盒:约 2 分钟内尝试 3 次
放弃平台未公布
出处https://developer.paddle.com/webhooks/about/respond-to-webhooks

签名

用一个经测试套件证明的可运行验证器核对,不是照着文档读来的。最近核对于 2026-08-20。

算法HMAC-SHA256
待签名串{timestamp}:{body}
编码hex
请求头paddle-signature = ts=X;h1=hex
时间容差5 秒

验证签名

要用原始请求体,逐字节一致,并且在任何 JSON 解析之前。框架一旦重新序列化请求体,本页的每一种方案都会失效。

const crypto = require("node:crypto");

// paddle-signature: "ts=1700000000;h1=abc..." — HMAC-SHA256 over "{ts}:{body}"
const pairs = Object.fromEntries(
  req.headers["paddle-signature"].split(";").map((p) => p.split("=")),
);
const expected = crypto
  .createHmac("sha256", secretKey) // per destination: pdl_ntfset_...
  .update(`${pairs.ts}:${rawBody}`)
  .digest("hex");
const received = Buffer.from(pairs.h1 ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
  received.length === wanted.length && crypto.timingSafeEqual(received, wanted);

Paddle 的 SDK 强制执行 5 秒的时间容差,是这里所有发送方中最紧的。如果前面有转发服务或队列,要用事件到达边缘节点的时间来校验时间戳,而不是你的 worker 处理到它的时间。

容易踩的坑

Stripe 的重试曲线稀疏的地方,Paddle 的很密:第一个小时约 10 次,第一天约 45 次,约 2 天内最多 60 次。一个只是慢了些的处理程序,起初每隔几分钟就会再运行一次,如果没有幂等处理,每次运行都会重复它的副作用。

沙盒和生产环境完全不同(约 2 分钟内尝试 3 次),所以在测试中看起来没问题的重试处理,只经受了 5% 的压力。

延伸阅读

AnyHook 挡在接收 Paddle Webhook 的端点前面:约 1 秒应答,你的服务器宕机时按自己的计划重试,并且每个事件都可以重放。 只改一个 URL,代码不用动;要接受重试和重放,就验证 AnyHook-Signature 请求头,因为 Paddle 自己的签名 5 秒后就会过期。

工作原理 →