🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 7 Min Lesezeit
0

How to Verify Paddle Billing Webhook Signatures (and the Raw-Body Bug That Breaks Them)

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Most Paddle webhook signature failures come down to one thing: you verified the signature against the wrong bytes. Your framework parsed the JSON, you re-serialized it, and the HMAC no longer matches what Paddle signed. The event looks tampered with even though nothing is wrong. This post shows the exact verification Paddle Billing expects, the raw-body trap that breaks it, and the difference between Paddle Billing and the older Paddle Classic scheme that sends people down the wrong path.



EventDock's own billing runs on Paddle Billing, so the code below is the same shape we use in production.






First: are you on Paddle Billing or Paddle Classic?



Paddle has two products with two completely different webhook signature schemes, and a lot of the older blog posts and Stack Overflow answers describe the wrong one.





  • Paddle Classic (the original product) sends a p_signature field inside the form-encoded body. It requires PHP serialization and is verified with Paddle's public key.


  • Paddle Billing (the current product) sends a Paddle-Signature HTTP header. It is an HMAC-SHA256 over the raw request body, keyed with a per-notification-destination secret. No public key needed.



If your webhook secret looks like pdl_ntfset_... and you see a Paddle-Signature header, you are on Paddle Billing. Accounts created after August 2023 are on Paddle Billing by default, and Classic is legacy. Everything below is for Paddle Billing.






What the Paddle-Signature header actually contains



Every Paddle Billing webhook arrives with a header like this:




CODE
Paddle-Signature: ts=1717000000;h1=eb4d0a2f...<64 hex chars>






Two fields, separated by a semicolon:





  • ts is a Unix timestamp (seconds) for when Paddle sent the request.


  • h1 is the HMAC-SHA256 digest, in lowercase hex.



The detail that the field names hide: Paddle does not sign the body on its own. It signs the string {ts}:{raw_body}. The timestamp is part of the signed material, which is what lets you reject replayed requests. Miss the ts: prefix and every signature you compute will be wrong even though your HMAC code is correct.






The raw-body trap



HMAC signs bytes, not objects. Paddle computed its digest over the exact bytes it put on the wire. If you hand your verification function anything other than those exact bytes, the digest will not match.



Here is how the bytes get changed without you noticing:




  1. Your web framework reads the request body and parses it into a JSON object for you.

  2. You call your verify function and pass it JSON.stringify(req.body) to turn the object back into a string.

  3. That re-serialized string is not byte-identical to what Paddle sent. Key order can change, whitespace disappears, unicode escapes differ, floating-point numbers get reformatted.

  4. Your HMAC is computed over different bytes, so h1 never matches, and you reject every real webhook as invalid.



The fix is always the same: verify against the raw, unparsed request body, then parse it only after the signature passes. Every framework has its own way to get the raw body, and getting it wrong is the most common cause of Paddle verification failures.






A correct verification in Node.js



It reads the two header fields, rebuilds the signed string as ts:body, computes the HMAC, compares in constant time, and rejects requests whose timestamp is too old to be fresh.




CODE
import crypto from 'crypto';

function verifyPaddleSignature(
rawBody: string, // the exact bytes Paddle sent, NOT a re-stringified object
signatureHeader: string,
secret: string // your notification destination secret, pdl_ntfset_...
): boolean {
// Header looks like: ts=1717000000;h1=abc123...
let ts = '';
let h1 = '';
for (const part of signatureHeader.split(';')) {
const [key, value] = part.split('=');
if (key === 'ts') ts = value;
if (key === 'h1') h1 = value;
}
if (!ts || !h1) return false;

// Reject replays: the timestamp is signed, so an attacker cannot forge a fresh one.
// Paddle's own examples use a tight 5-second window. Widen it a little if your
// servers see clock skew, but keep it short.
const age = Math.floor(Date.now() / 1000) - Number(ts);
if (age > 5) return false;

// Paddle signs the string `${ts}:${rawBody}`, not the body alone.
const signedPayload = `${ts}:${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');

// Constant-time compare to avoid leaking the digest through timing.
const a = Buffer.from(h1);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}






Notice there is no JSON.parse anywhere in that function. Parsing happens after it returns true, in your handler, never before.






Getting the raw body in Express, Next.js, and Cloudflare Workers



The verify function is the simple part. Feeding it the untouched body is where frameworks fight you.






Express



The default express.json() middleware consumes the stream and leaves you only the parsed object. Use the raw parser on the webhook route so you keep the bytes:




CODE
app.post('/webhooks/paddle',
express.raw({ type: 'application/json' }), // req.body is now a Buffer
(req, res) => {
const sig = req.get('Paddle-Signature') ?? '';
if (!verifyPaddleSignature(req.body.toString('utf8'), sig, process.env.PADDLE_WEBHOOK_SECRET)) {
return res.status(400).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).send('ok'); // ack fast, process after
handlePaddleEvent(event).catch(console.error);
}
);









Next.js (App Router)



Route handlers give you the raw body directly through await req.text(). Do not call req.json() first, because you cannot read the body twice.




CODE
export async function POST(req: Request) {
const raw = await req.text(); // exact bytes
const sig = req.headers.get('paddle-signature') ?? '';
if (!verifyPaddleSignature(raw, sig, process.env.PADDLE_WEBHOOK_SECRET!)) {
return new Response('bad signature', { status: 400 });
}
const event = JSON.parse(raw);
return Response.json({ ok: true });
}









Cloudflare Workers



Workers give you the raw text with await request.text() and ship the Web Crypto API, so you do not even need the Node crypto module. This is close to what EventDock runs.




CODE
const raw = await request.text();
const sig = request.headers.get('paddle-signature') ?? '';
// verify with crypto.subtle.importKey + crypto.subtle.sign('HMAC', ...)
// compare the hex digest to h1









A checklist when it still fails



If you did all that and Paddle webhooks still read as invalid, walk this list in order:





  • Are you signing ts:body and not just body? This is the second most common miss after the raw body.


  • Is the secret the right one? Paddle Billing gives each notification destination its own secret. If you have a sandbox destination and a production destination, they have different secrets. A sandbox secret against production traffic fails silently.


  • Sandbox versus production. Sandbox events come from your sandbox destination and must be checked with the sandbox secret. Do not share one secret across both.


  • Is a proxy rewriting the body? Some API gateways and body-logging middleware re-encode JSON before it reaches your handler. Check the bytes at the very edge.


  • Is h1 compared as lowercase hex? Paddle sends lowercase. If you uppercase or Base64 anywhere, the compare fails.






Verification is step one. Delivery is the real problem.



Signature verification tells you a webhook is authentic. It does nothing for the webhooks that never reach you, or the ones you drop because your handler was mid-deploy when Paddle called.



Paddle Billing retries a failed webhook with backoff over about three days, then stops. That window sounds forgiving until a bad deploy, a database timeout, or a cold start eats a burst of subscription.activated and transaction.completed events during the minutes a customer is actually paying you. Lose one and you get a paying customer stuck on a free-tier account, or a churned customer your system still thinks is active.



This is the problem EventDock solves. You point Paddle at EventDock instead of directly at your app. EventDock verifies the signature, stores the event, and acknowledges Paddle right away so the delivery is safely captured before the retry window matters, then forwards it to your app with its own retries and a dead-letter queue you can replay by hand. If your app is down for an hour, the events wait in the queue and arrive once it recovers.



You can wire it up with the free tier and watch every Paddle event get captured and delivered. handles the duplicate deliveries that every retrying provider, Paddle included, will eventually send you.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How to Verify Paddle Billing Webhook Signatures (and the Raw-Body Bug That Breaks Them)

Thematisch verwandte Begriffe: Verify, Paddle, Billing, Webhook · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...