投递行为
取自该平台的官方文档,最近核对于 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 秒后就会过期。