Adyen HMAC verification failing? Check these 7 causes, in order

Adyen's webhook HMAC is unlike almost every other provider's: it doesn't sign the raw request body at all. It signs a colon-joined string of 8 extracted fields, keyed with a hex-decoded key, output in base64. Each of those three properties breaks a habit you learned verifying GitHub- or Stripe-style webhooks, and each is a distinct way to fail. Here's the exact algorithm, then the causes in the order to check them.

What Adyen actually signs (standard payment webhooks)

For each NotificationRequestItem in the body, the signature in additionalData.hmacSignature is:

signed_string = pspReference + ":" + originalReference + ":" + merchantAccountCode
              + ":" + merchantReference + ":" + amount.value + ":" + amount.currency
              + ":" + eventCode + ":" + success

signature = base64( HMAC_SHA256( key = hex_decode(hmac_key), message = signed_string ) )

Empty fields stay empty — originalReference is usually absent on an AUTHORISATION, so the signed string contains a double colon. Adyen's own docs provide a full test vector (reproduced at the bottom of this page) — use it before you touch a real delivery.

1. You're using the hex characters as the key

This is the Adyen HMAC bug. The Customer Area gives you the key as a hex string like

44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056

and every official library decodes it to 32 raw bytes before keying the HMAC — Buffer.from(key, "hex") in Node, binascii.a2b_hex in Python. Feed the 64 hex characters in directly and you get a stable, plausible-looking, wrong signature. With Adyen's documented test vector, the hex-chars-as-key mistake computes

v1SgtPdCljLGt5Ln1m/87X4DF+iNzvtUfStAjQlfiWw=   ← wrong (hex chars as key)
coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=   ← correct (key hex-decoded)

If your computed value matches the first form, this is your bug.

2. The signed string is built wrong

3. You're escaping colons and backslashes

Blog posts (and some AI-generated answers) tell you to escape : and \ in the field values. In Adyen's official Node and Java libraries that escaping only applies to the legacy sorted-map variant (classic HPP payments) — not to NotificationRequestItem field signing. If you added an escaping pass, remove it; for standard payment webhooks the values are joined verbatim.

4. You're verifying the raw body

Generic HMAC testers — and most webhook middleware — compute HMAC over the raw request body. Adyen's payment webhooks structurally can't be verified that way: there is no raw-body signature. (The upside of Adyen's design: a proxy that re-serializes JSON or reorders keys can't break the signature.) You need a verifier that knows the field-extraction scheme — this in-browser debugger implements it (100% client-side; the key never leaves your machine), or set the Adyen scheme on a capture bin (below) for live ✓/✗ badges on real deliveries.

5. You're comparing in the wrong encoding

hmacSignature is base64. If your HMAC library returns hex digests by default (Python's hexdigest(), Node's digest('hex')), the comparison never matches even when the computation is right. Compare base64 to base64 — constant-time.

6. It's a Banking or Management webhook — different scheme entirely

Adyen's "other" webhook families (Banking / platform / Management API) do not embed a signature in the body. They send a base64 HMAC-SHA256 over the raw body in an hmacsignature header, alongside protocol: HmacSHA256 — same hex-encoded key format, still hex-decode it. If your endpoint receives both families, branch on whether the body has notificationItems. (A capture bin with the Adyen scheme applies the same fallback automatically.)

7. Batches and key rotation

Prove your implementation against Adyen's own test vector

From Adyen's docs (key, payload and expected signature all documented):

const crypto = require('crypto');
const key = '44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056';
const signed = '7914073381342284::TestMerchant:TestPayment-1407325143704:1130:EUR:AUTHORISATION:true';
const sig = crypto.createHmac('sha256', Buffer.from(key, 'hex'))
                  .update(signed, 'utf8').digest('base64');
// → coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=

If this vector passes but real deliveries fail, the bug is in your field extraction (cause 2) — capture a real delivery in a bin and rebuild the signed string from the actual bytes.

While you're failing: the retry queue is sequential

Adyen expects any 2xx within 10 seconds (status code alone — the legacy [accepted] body is no longer required). On failure it retries at 9s/18s/27s, then from a queue at 2m/5m/10m/15m/30m/1h/2h/4h/8h intervals for up to 30 days. Two consequences people miss:

Failure alerts fire after 5 attempts and again at 7 days; the Customer Area has manual Retry/Ignore per event.

Debug with a capture bin

Create a free bin, set the signature scheme to Adyen in bin settings, paste your hex HMAC key, and point a test webhook at the capture URL (Customer Area → webhook → Test configuration). Every delivery gets a live ✓/✗ badge per item — if Adyen's own delivery verifies ✓ against your key but your handler says invalid, the bug is in your code, not your key. The bin answers 200 within Adyen's 10-second deadline, so nothing queues up while you debug.

Related: the Adyen AUTHORISATION payload example (its signature actually verifies against the documented sample key), the 23-provider retry-schedule comparison, and Square / HubSpot — the other providers generic raw-body testers structurally can't verify.

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

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