The loop
Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.
What's different about Shopify
Shopify's answer to a failing endpoint doesn't stop at the failed delivery. After eight consecutive failures the subscription itself is removed, for subscriptions created through the Admin API — that's what Shopify documents, not an edge case. The delivery budget is one second to connect and five seconds for the entire request, and any status outside the 200 range counts as an error, 3XX redirects included. Put a tunnel in front of your laptop and a debugger breakpoint doesn't only cost you one event — it can quietly unregister the thing you were testing. Acknowledge from a capture URL and replay at your own pace.
Where the URL goes
Store owners create subscriptions in the Shopify admin under Settings, then Notifications, then Webhooks, where Create webhook asks for the event, JSON or XML, the destination URL, and a webhook API version. The event can't be changed after creation, so a wrong pick means a fresh subscription. App developers instead put the ingest URL in the uri key of a webhooks.subscriptions block in shopify.app.toml, alongside a topics array holding the events they want, or create a shop-specific subscription with the webhookSubscriptionCreate mutation on the GraphQL Admin API.
Every saved webhook in the admin carries a Send test option behind the ... overflow menu on its row, which fires a sample notification at the URL you registered. App builds get shopify app webhook trigger, which delivers a sample Admin API topic payload to an address you name, localhost ports included. Two caveats straight from Shopify's CLI reference: the triggered payload never varies, and triggered webhooks aren't retried when they fail.
What Shopify sends
Topics are lowercase resource/action pairs split by a slash — noun first, verb second. The GraphQL Admin API publishes the same set as SCREAMING_SNAKE_CASE enums, so ORDERS_CREATE and orders/create are one topic in two dialects. Mind the spelling: Shopify writes cancelled with two Ls.
- orders/create
- orders/paid
- orders/updated
- orders/cancelled
- products/update
- app/uninstalled
Worth knowing first
Five seconds, then it's an error
One second to open the connection, five seconds for the whole request. Anything outside the 200 range is failure, and that explicitly includes 3XX — so a trailing-slash redirect on your route is scored as a failed delivery rather than a hop Shopify takes. Acknowledge with 200 OK first, do the work afterwards.
Failed deliveries can unregister you
A failure earns eight retries, spread over four hours per Shopify's HTTPS delivery page, and eight consecutive failures delete an Admin-API-created subscription. Shopify's own troubleshooting page words the removal window as a 24-hour period instead, so treat the exact figure as unsettled and the outcome as real.
Duplicates arrive, order doesn't hold
Shopify doesn't promise ordering within a topic or across topics touching the same resource, and says outright that delivery isn't assured. Deduplicate on X-Shopify-Webhook-Id, sequence with X-Shopify-Triggered-At or the payload's updated_at, and keep a reconciliation job that reads the API for whatever never turned up.
localhost isn't a deliverable destination
Shopify won't deliver webhooks to localhost, to any URL ending in the word internal, to fake domains, to Shopify domains, or to a custom domain attached to the store. Deliveries go out to an https:// URI with SSL certificates verified on delivery, so the destination has to be publicly reachable and terminate TLS properly.
Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.