CommerceHook

Shopify webhooks: the subscription that deletes itself, and the signature that will not match

Most platforms punish a broken endpoint by going quiet. Shopify eventually removes the subscription instead, which is a different kind of problem: there is nothing left to inspect, nothing in your code changed, and the thing that broke happened hours ago.

Eight retries, four hours, then it is gone

A failed delivery is retried eight times across roughly four hours on an exponential backoff. If the failures continue, a subscription created through the Admin API is deleted automatically. Shopify emails the app's emergency developer address before that happens, which is worth exactly as much as the attention that inbox gets.

Older write-ups still quote nineteen attempts over forty-eight hours. That policy is long dead, and the difference matters: four hours is short enough that a deploy going wrong overnight can cost you the subscription before anybody looks. An endpoint that always answers 2xx keeps the subscription alive while you fix the handler behind it, which is most of the argument for pointing topics at an inspector rather than straight at your own service.

The signature covers the raw body, keyed with the client secret

X-Shopify-Hmac-Sha256 is an HMAC-SHA256 over the raw request body, keyed with your app's client secret, and base64 encoded rather than hex. Each of those three details is a way to spend an afternoon: a framework that parses the body first changes the bytes being signed, the API access token is not the signing key, and a hex comparison never matches no matter how correct everything else is.

One timing trap. After you rotate the client secret, Shopify can take up to an hour to start signing with the new one, so a verification that accepts only the new key will reject real deliveries for that hour. Accept both during the changeover.

Two ids, and only one of them deduplicates

A repeated event is either a retry or a second subscription, and Shopify lets you tell them apart. X-Shopify-Webhook-Id is stable across retries of one delivery, so that is the key to deduplicate on. X-Shopify-Event-Id is shared by every delivery caused by one merchant action, so two subscriptions on the same topic produce two webhook ids and one event id.

Reach for the event id as your dedupe key and you will quietly drop deliveries you meant to receive, which is a bug that looks like missing data rather than like a mistake in your handler.

Registration is GraphQL, and the topic is spelled twice

Subscriptions are created with the webhookSubscriptionCreate mutation on the GraphQL Admin API, against a custom app's Admin API access token. The topic is an enum there, ORDERS_CREATE, while the delivery header carries the slash form, orders/create. Both spellings are correct and you will grep for the wrong one at least once.

The API version in the request URL fixes the payload version for that subscription, so a subscription created a year ago keeps delivering the shape it was created with until you change it.

The headers that matter

Treat the names as case-insensitive, which is Shopify's own advice: HTTP/2 lowercases them often enough that matching on the documented casing is a bug waiting to happen.

Debugging questions

Why did my Shopify webhook subscription disappear?
Because it kept failing. Shopify retries a failed delivery eight times across about four hours on an exponential backoff, and a subscription that keeps failing is deleted automatically when it was created through the Admin API. Warning emails go to the app’s emergency developer address first, which helps only if somebody reads that inbox. This is the single most surprising thing about Shopify webhooks: the endpoint does not just go quiet, the subscription stops existing, and nothing in your code changed.
Why is my HMAC verification failing?
Three usual causes. The body was parsed before it was verified, which changes the bytes and so changes the digest; verify against the raw body first. The wrong key was used: the signature is an HMAC-SHA256 over the raw body keyed with the app’s client secret, base64 encoded, not hex. Or the secret was rotated recently, in which case Shopify can take up to an hour to start signing with the new one, so both keys are worth accepting during the changeover.
Why did I receive the same event twice?
Either a retry of one delivery, or two subscriptions on the same topic. The two cases are distinguishable and the distinction matters: retries of one delivery repeat the same X-Shopify-Webhook-Id, so deduplicate on that. Separate subscriptions produce different webhook ids but share one X-Shopify-Event-Id, because a single merchant action caused both. Deduplicating on the event id instead would silently drop a delivery you meant to receive.
Do I have to use GraphQL to register webhooks?
For anything new, yes in practice. Subscriptions are created with the webhookSubscriptionCreate mutation on the GraphQL Admin API. The topic is a screaming-case enum there (ORDERS_CREATE) while the delivery header carries the slash form (orders/create), which is a small thing that wastes an afternoon the first time you grep your logs for the wrong one.

Seeing the deliveries

CommerceHook gives a Shopify store a stable HTTPS destination that always answers, which is the part that keeps a subscription from being deleted while you fix whatever is behind it. Every delivery shows up in an inspector: the topic, every header including the signature and both ids, and the payload as a JSON tree or raw. Topics are registered from the dashboard over the GraphQL Admin API, deliveries can be replayed at your own handler once it is working, and commercehook listen streams them into the one on your laptop. Free for one endpoint, no card.

The same endpoint takes BigCommerce, WooCommerce, Square and Stripe, so a store running Shopify beside a Stripe checkout reads both in one timeline.