Skip to content
Back to site

Receive Adyen webhooks reliably

Adyen sends its webhooks to a Railhook source. The source verifies the HMAC, stores each one as it arrived and forwards it to your service with retries. Railhook acknowledges Adyen immediately, so your payment handler’s speed no longer matters to Adyen.

Adyen wants a 2xx within 10 seconds. If it does not get one, it tries again at 9, 18 and 27 seconds, then moves the item into a retry queue: 2 minutes, 5, 10, 15, 30, an hour, two, four, eight, and on at widening intervals for up to 30 days. (Adyen: Troubleshoot webhooks)

The queue is per endpoint, and a webhook stuck in it is one your reconciliation is waiting on. With Railhook in front:

  • Adyen is answered as soon as the webhook is stored, well inside the 10 seconds, so nothing enters Adyen’s retry queue because of your app.
  • Your service gets up to 5 attempts per forward, with waits from 1 minute to 1 hour, and what still fails lands in Failed Forwards.
  • A duplicate (the same pspReference and eventCode) is answered with the stored copy and not forwarded twice.
  • Anything stored can be replayed, on your schedule rather than Adyen’s.

You need a Railhook project and an API key. The same steps are available under Incoming in the dashboard.

Terminal window
export RAILHOOK_URL=https://railhook.io # or your own instance
export RAILHOOK_API_KEY=...
export PROJECT_ID=...
  1. Create the webhook in Adyen. In the Customer Area, go to Developers → Webhooks and click Create new webhook. For ordinary payment events pick the Standard notification type. You need the Merchant Technical Integrator or Merchant Admin role to do this. (Adyen: Configure and manage webhooks)

  2. Create the Railhook source to get the URL Adyen should post to:

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"Adyen","providerType":"ADYEN","verificationMode":"NONE"}'

    Keep the id and the ingressUrl, and paste the ingressUrl into the webhook’s Server configuration → URL in Adyen.

  3. Generate the HMAC key. In the Security section of the same webhook screen, click Generate new HMAC key and copy it at once: Adyen shows it in full only that one time. Afterwards only its key check value stays visible.

  4. Give Railhook the key and turn verification on:

    Terminal window
    curl -X PUT "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources/$SOURCE_ID" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"Adyen","verificationMode":"PROVIDER","hmacSecret":"'"$ADYEN_HMAC_KEY"'"}'

    Paste the key exactly as Adyen printed it: a long hexadecimal string. Railhook decodes it to bytes before it keys the HMAC, which is what Adyen’s own libraries do.

  5. Add a destination, the URL of your own service:

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources/$SOURCE_ID/destinations" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://api.example.com/webhooks/adyen","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}'
  6. Test it from the Customer Area. Open the saved webhook, use Test configuration, pick an event, and Adyen sends sample notifications and reports the result inline. They appear under Incoming → Received, with one forward per destination.

Adyen signs in two different shapes, and one Railhook source verifies either, because the request itself says which is in play.

Standard payments webhooks carry no signature header at all. Each notificationItems[].NotificationRequestItem signs eight of its own fields, joined with colons:

pspReference:originalReference:merchantAccountCode:merchantReference:value:currency:eventCode:success

An absent field is written as an empty string, and nothing is escaped. The base64 HMAC-SHA256 of that string lands in the item’s additionalData.hmacSignature. Railhook checks every item in the request: a notification is accepted or refused whole, so one forged item cannot travel with a genuine one. (Adyen: Verify HMAC signatures)

Management, Banking and Balance Platform webhooks put a base64 HMAC-SHA256 of the entire raw body into an hmacsignature header, keyed with the same hex key.

A request that does not verify is answered 401 and is not stored.

Adyen documents no timestamp and no tolerance window, so nothing binds a signature to a moment. What bounds a repeat is the replay window, and ahead of it the pspReference and eventCode pair, which Adyen says two copies of one event share. A notification carrying exactly one item is deduplicated on that pair; one carrying several is not, because a key taken from the first item would answer a later request with an earlier one and drop the items it did not name.

The body exactly as Adyen sent it, byte for byte, notificationItems and all. Your service still has to answer Railhook, not Adyen, so it does not need to return Adyen’s acknowledgement: Railhook has already sent that. See Destinations.

If your handler would rather receive one item than an envelope, set the destination’s payloadTransform to $.notificationItems[0].NotificationRequestItem, which forwards the item itself. Only do that where you know Adyen sends one item per request: the expression keeps the first and discards the rest. See Transformations.

  1. Install the CLI and log in, as in CLI, then open a tunnel to your app’s port:

    Terminal window
    railhook listen 3000
  2. Add a second destination to the source with the printed public URL and your route appended, such as https://<host>/tunnel/<slug>/webhooks/adyen. It reaches http://localhost:3000/webhooks/adyen.

  3. Use Test configuration in the Customer Area, and the sample notifications arrive in your local app. Disable that destination when you stop the tunnel.

Each replayed notification goes to every destination of the source as a new forward:

Terminal window
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-events/bulk-replay" \
-H "X-API-Key: $RAILHOOK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sourceId":"'"$SOURCE_ID"'","from":"2026-09-18T10:00:00Z","to":"2026-09-18T14:00:00Z","verified":true}'

Replay reaches back as far as events are kept: 7 days on Railhook Cloud, and whatever you configure when self-hosting. Keep your handler idempotent on pspReference and eventCode, since a replay can send notifications your service already processed. See Replay.