Receive Shopify webhooks reliably
Shopify sends its webhooks to a Railhook source. The source verifies the HMAC, stores each webhook as it arrived and forwards it to your service with retries. Railhook answers Shopify 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) Deliveries should be deduplicated by X-Shopify-Webhook-Id. (Shopify: Deliver webhooks through HTTPS)
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. The same steps are available 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”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.