$npx -y skills add hookdeck/webhook-skills --skill adyen-webhooksReceive and verify Adyen webhooks (standard notifications). Use when setting up Adyen webhook handlers, debugging HMAC signature verification, or handling payment events like AUTHORISATION, CAPTURE, REFUND, CANCELLATION, and CHARGEBACK.
| 1 | # Adyen Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - How do I receive Adyen webhooks (standard notifications)? |
| 6 | - How do I verify Adyen webhook HMAC signatures? |
| 7 | - How do I handle AUTHORISATION, CAPTURE, REFUND, or CHARGEBACK events? |
| 8 | - Why is my Adyen HMAC signature verification failing? |
| 9 | - What response body does Adyen expect from my webhook endpoint? |
| 10 | |
| 11 | ## How Adyen Webhooks Work |
| 12 | |
| 13 | Adyen sends **standard notifications** as an HTTP POST with a JSON body containing |
| 14 | a batch of `notificationItems`. Each item wraps a `NotificationRequestItem`: |
| 15 | |
| 16 | ```json |
| 17 | { |
| 18 | "live": "false", |
| 19 | "notificationItems": [ |
| 20 | { |
| 21 | "NotificationRequestItem": { |
| 22 | "eventCode": "AUTHORISATION", |
| 23 | "success": "true", |
| 24 | "pspReference": "7914073381342284", |
| 25 | "merchantAccountCode": "TestMerchant", |
| 26 | "merchantReference": "TestPayment-1407325143704", |
| 27 | "amount": { "value": 1130, "currency": "EUR" }, |
| 28 | "additionalData": { "hmacSignature": "coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=" } |
| 29 | } |
| 30 | } |
| 31 | ] |
| 32 | } |
| 33 | ``` |
| 34 | |
| 35 | Your endpoint must: |
| 36 | |
| 37 | 1. **Verify the HMAC signature on each item** (`additionalData.hmacSignature`). |
| 38 | 2. **Acknowledge with the literal body `[accepted]`** and HTTP 200 — otherwise Adyen retries. |
| 39 | |
| 40 | ## Verification (core) |
| 41 | |
| 42 | Adyen's HMAC is **not** computed over the raw request body. It is computed over a |
| 43 | `:`-delimited string of specific fields, in this exact order: |
| 44 | |
| 45 | ``` |
| 46 | pspReference : originalReference : merchantAccountCode : merchantReference : amount.value : amount.currency : eventCode : success |
| 47 | ``` |
| 48 | |
| 49 | Each field value is escaped (`\` → `\\`, `:` → `\:`), empty fields become empty |
| 50 | strings, and the HMAC key from the Customer Area is a **hex string that must be |
| 51 | hex-decoded** before use. The result is HMAC-SHA256, base64-encoded, and compared |
| 52 | against `additionalData.hmacSignature`. Because the signature covers parsed fields, |
| 53 | you parse the JSON first, then verify each item. |
| 54 | |
| 55 | The official `@adyen/api-library` SDK does all of this for you: |
| 56 | |
| 57 | ```javascript |
| 58 | const { hmacValidator } = require('@adyen/api-library'); |
| 59 | |
| 60 | const validator = new hmacValidator(); |
| 61 | |
| 62 | // item = notificationItems[i].NotificationRequestItem (plain parsed object) |
| 63 | // ADYEN_HMAC_KEY = hex string from the Customer Area |
| 64 | const valid = validator.validateHMAC(item, process.env.ADYEN_HMAC_KEY); |
| 65 | // reads item.additionalData.hmacSignature and compares (timing-safe) internally |
| 66 | ``` |
| 67 | |
| 68 | No Node SDK (e.g. Python/FastAPI)? Reproduce the algorithm manually: |
| 69 | |
| 70 | ```python |
| 71 | import hmac, hashlib, base64, binascii |
| 72 | |
| 73 | def calculate_hmac(item, hex_key): |
| 74 | a = item.get("amount") or {} |
| 75 | fields = [item.get("pspReference", ""), item.get("originalReference", ""), |
| 76 | item.get("merchantAccountCode", ""), item.get("merchantReference", ""), |
| 77 | a.get("value", ""), a.get("currency", ""), |
| 78 | item.get("eventCode", ""), item.get("success", "")] |
| 79 | data = ":".join(str(f).replace("\\", "\\\\").replace(":", "\\:") for f in fields) |
| 80 | key = binascii.unhexlify(hex_key) # hex → bytes |
| 81 | return base64.b64encode(hmac.new(key, data.encode("utf-8"), hashlib.sha256).digest()).decode() |
| 82 | |
| 83 | # hmac.compare_digest(calculate_hmac(item, key), item["additionalData"]["hmacSignature"]) |
| 84 | ``` |
| 85 | |
| 86 | > **For complete handlers with route wiring, event dispatch, Basic Auth, and tests**, see: |
| 87 | > - [examples/express/](examples/express/) |
| 88 | > - [examples/nextjs/](examples/nextjs/) |
| 89 | > - [examples/fastapi/](examples/fastapi/) |
| 90 | |
| 91 | ## Common Event Types |
| 92 | |
| 93 | | `eventCode` | Triggered When | |
| 94 | |-------------|----------------| |
| 95 | | `AUTHORISATION` | A payment was authorised (check `success` for the outcome) | |
| 96 | | `CAPTURE` | Authorised funds were captured | |
| 97 | | `CAPTURE_FAILED` | A capture attempt failed | |
| 98 | | `REFUND` | A refund was processed | |
| 99 | | `REFUND_FAILED` | A refund attempt failed | |
| 100 | | `CANCELLATION` | An authorisation was cancelled | |
| 101 | | `CANCEL_OR_REFUND` | A payment was cancelled or refunded | |
| 102 | | `CHARGEBACK` | Funds were reversed by the shopper's bank | |
| 103 | | `NOTIFICATION_OF_CHARGEBACK` | A chargeback dispute was opened | |
| 104 | | `REPORT_AVAILABLE` | A generated report is ready to download | |
| 105 | |
| 106 | > **Important:** Always check the `success` field — an `AUTHORISATION` with |
| 107 | > `success: "false"` means the payment was refused, not approved. |
| 108 | |
| 109 | > **For the full event reference**, see [Adyen webhook types](https://docs.adyen.com/development-resources/webhooks/webhook-types). |
| 110 | |
| 111 | ## Environment Variables |
| 112 | |
| 113 | ```bash |
| 114 | ADYEN_HMAC_KEY=YOUR_HEX_HMAC_KEY # Hex string generated in the Customer Area |
| 115 | # Optional Basic Auth (recommended) — configured alongside the webhook in the Customer Area |
| 116 | ADYEN_WEBHOOK_USERNAME=your_username |
| 117 | ADYEN_WEBHOOK_PASS |