Delivery behaviour
Read from the vendor's own documentation, last checked 2026-08-06. Where the vendor states no number, this table carries none.
| Response budget | 5s |
|---|---|
| Retry | Live: 60 attempts over 3 days, 20 in the first hour, 47 in the first day. Sandbox: 3 attempts over 15 min |
| Gives up | not published by the vendor |
| Source | https://developer.paddle.com/webhooks/about/respond-to-webhooks |
Signature
Verified against a working verifier proven by a test suite, not read from documentation. Last verified 2026-08-20.
| Algorithm | HMAC-SHA256 |
|---|---|
| Signed payload | {timestamp}:{body} |
| Encoding | hex |
| Header | paddle-signature = ts=X;h1=hex |
| Tolerance | 5s |
Verify it
The raw request body, byte for byte, before any JSON parsing. Every scheme on this page breaks the moment a framework re-serializes the payload.
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 valid = crypto.timingSafeEqual(
Buffer.from(pairs.h1),
Buffer.from(expected),
);Paddle's SDK enforces a 5-second timestamp tolerance, the tightest of any sender here. Behind a relay or a queue, check the timestamp against when the event arrived at the edge, not when your worker got to it.
What bites
The retry curve is dense where Stripe's is sparse: 20 attempts in the first hour, 47 in the first day, 60 over 3 days. A handler that is merely slow gets hit again while still processing the last attempt, which is how duplicate side effects happen at 2x the usual rate.
Sandbox behaves nothing like live — 3 attempts over 15 minutes — so retry handling that looks fine in testing has seen 5% of the pressure.
Read more
AnyHook sits in front of endpoints that receive from Paddle: it answers inside the budget above, retries on its own schedule when your server is down, and keeps every event replayable. Change one URL, keep your code.