Receive Shopify webhooks reliably
This guide puts Railhook between Shopify and your app. Shopify sends its webhooks to a Railhook source, which verifies the HMAC, stores each webhook as it arrived and forwards it to your service with retries. Shopify only ever talks to Railhook, which answers straight away, so a slow or failing app no longer puts the subscription at risk.
Why Shopify webhooks need a buffer
Section titled “Why Shopify webhooks need a buffer”Shopify expects your app to respond within five seconds. It retries a failed webhook up to eight times over four hours, and if failures continue, the subscription is removed. (Shopify: Troubleshooting webhooks) Shopify also tells you to deduplicate deliveries by X-Shopify-Webhook-Id. (Shopify: Deliver webhooks through HTTPS)
Four hours is a short window for a bad deploy, and five seconds is a tight budget for an order handler. With Railhook in front:
- Shopify is answered as soon as the webhook is stored, well inside the five seconds.
- 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 second delivery with the same
X-Shopify-Webhook-Idis answered with the stored copy and not forwarded again. - Any stored webhook can be replayed for as long as events are kept, long after Shopify’s four hours.
Connect Shopify
Section titled “Connect Shopify”You need a Railhook project and an API key. Everything below can also be done under Incoming in the dashboard.
export RAILHOOK_URL=https://railhook.io # or your own instanceexport RAILHOOK_API_KEY=...export PROJECT_ID=...-
Create the source with your app’s client secret. Shopify computes
X-Shopify-Hmac-SHA256with it, so it is the secret Railhook verifies against: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":"Shopify","providerType":"SHOPIFY","verificationMode":"PROVIDER","hmacSecret":"'"$SHOPIFY_CLIENT_SECRET"'"}'Keep the
idand theingressUrlfrom the response. -
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/shopify","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}' -
Subscribe in
shopify.app.toml, with theingressUrlas theuri:shopify.app.toml [webhooks]api_version = "2026-07"[[webhooks.subscriptions]]topics = ["orders/create"]uri = "https://railhook.io/ingress/<token>"Deploy it with
shopify app deploy. Whileshopify app devruns, a saved change applies to your development store straight away. -
Send a test:
Terminal window shopify app webhook trigger --api-version=2026-07 --topic=orders/create --address="$INGRESS_URL"It appears under Incoming → Received, with one forward per destination.
How the signature is checked
Section titled “How the signature is checked”Railhook computes the HMAC-SHA256 of the raw body with the client secret, base64-encodes it and compares it with X-Shopify-Hmac-SHA256. A request that does not match is answered 401 and not stored. A signature already seen within the replay window, 5 minutes by default, is refused too, unless the request is a resend of a webhook Railhook already stored.
What your service receives
Section titled “What your service receives”The body exactly as Shopify sent it, byte for byte. Railhook passes Shopify’s event headers through: X-Shopify-Topic, X-Shopify-Webhook-Id, X-Shopify-Shop-Domain, X-Shopify-API-Version, X-Shopify-Event-Id and X-Shopify-Triggered-At. X-Shopify-Hmac-SHA256 is not forwarded: your service authenticates Railhook through the destination’s credentials, and does not need the client secret. See Destinations.
Develop locally with the CLI
Section titled “Develop locally with the CLI”-
Install the CLI and log in, as in CLI, then open a tunnel to your app’s port:
Terminal window railhook listen 3000 -
Add a second destination to the source with the printed public URL and your route appended, such as
https://<host>/tunnel/<slug>/webhooks/shopify. It reacheshttp://localhost:3000/webhooks/shopify. -
Trigger a test webhook, or create an order in your development store, and it arrives in your local app with its
X-Shopify-Topicheader. Disable that destination when you stop the tunnel.
Replay after an outage
Section titled “Replay after an outage”Replay what arrived while your service was down. Each replayed webhook goes to every destination of the source as a new forward:
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 X-Shopify-Webhook-Id, since a replay can send webhooks your service already processed. See Replay.