Patreon webhook signature verification failing? It's MD5

If your Patreon webhook verifier is copy-pasted from a GitHub or Stripe integration, it will never match — not because of a subtle byte issue, but because Patreon uses a different hash. Patreon signs every webhook with HMAC-MD5, hex-encoded, over the raw request body. No sha256=-style prefix, no timestamp, no base64. Per docs.patreon.com (verified 2026-08-30), X-Patreon-Signature is "the HEX digest of the message body HMAC signed (with MD5) using your webhook's secret". Patreon is, as far as I can find, the only major provider still signing with MD5. Here are the failure causes in the order to check them.

What Patreon actually sends

POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Patreon-Event: members:pledge:create
X-Patreon-Signature: ce471d0a0b33ed30125fa25e1a4d6c8d

{ "data": { "type": "member", ... }, "included": [...], "links": {...} }

The signature is computed as:

signature = hex( HMAC_MD5( key = webhook_secret, message = raw_body ) )

Note the digest is 32 hex chars (128-bit MD5), not the 64 you'd get from SHA-256 — that length alone tells you which hash you're dealing with.

Cause 1: you assumed SHA-256

Every muscle-memory verifier — GitHub's sha256=, Stripe's v1=, Shopify's base64 — is HMAC-SHA256. Swap the hash and everything else about your pipeline can be correct while nothing ever matches:

# Node
crypto.createHmac('md5', secret).update(rawBody).digest('hex')

# Python
hmac.new(secret.encode(), raw_body, hashlib.md5).hexdigest()

# CLI sanity check
openssl dgst -md5 -hmac "$SECRET" < body.bin

Cause 2: your runtime has no MD5 at all

This is the failure unique to Patreon. crypto.subtle — the WebCrypto API used in browsers, Cloudflare Workers, and Deno-style runtimes — does not implement MD5 in any form. importKey with { hash: 'MD5' } throws. So WebCrypto-based verifier snippets (and most online HMAC testers, which are built on WebCrypto) fail before they compute anything. Your options on those runtimes are a pure-JS MD5 implementation (RFC 1321 is ~100 lines) or verifying somewhere Node's crypto is available. This site's signature debugger and capture bins ship a pure-JS RFC-1321 HMAC-MD5 precisely for this — the debugger stays 100% client-side.

Cause 3: re-serialized body

The classic, and it bites harder here because Patreon's JSON:API payloads are large and deeply nested: the HMAC covers the raw request bytes, so any framework that parses and re-stringifies JSON before your verifier runs (key reordering, whitespace, +00:00 timezone formats in timestamps) produces a different digest. Verify against the raw body, then parse.

Cause 4: wrong secret — but not for the reason you think

Patreon's webhook secret is not show-once (unlike Stripe, Mux or Frame.io): you can read it any time on the Platform portal webhooks page, or as the secret field on API webhook objects (GET /api/oauth2/v2/webhooks). So "I lost the secret" is never the issue — but each webhook has its own secret, so if you registered one webhook via the portal and another via the API, verifying portal deliveries with the API webhook's secret fails every time.

Not a signature bug: branching on payload shape

A near-miss that looks like corrupt payloads: one Patreon webhook can subscribe to several triggers (members:create/update/delete, members:pledge:create/update/delete, posts:publish/update/delete) and every members:* delivery is the same type: "member" JSON:API document. The X-Patreon-Event header is the only reliable discriminator. Also: members:pledge:create fires when an existing follower upgrades to patron, and members:create can fire more than once per person (delete + renew) — dedupe on the member UUID (data.id). And if your payload has type: "pledge" with integer IDs, you're reading an old APIv1 tutorial — those pledge:* triggers are deprecated.

Check a real delivery in one minute

Point your Patreon webhook at a Hookden bin, set the bin's signature scheme to Patreon and paste your secret — every capture gets a live ✓/✗ badge computed from the exact bytes Patreon sent. If the badge is ✓ but your handler rejects the same delivery, the bug is your body handling, not your key. Or poke at the members:pledge:create payload example — its signature actually verifies against the documented sample secret.

While you're here: deliveries stop silently when the webhook pauses

Related: retry-schedule comparison across 23 providers, Stripe signature failures, and webhooks to localhost for developing your handler behind the bin.

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site