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.
X-Shopify-Topic: the slash-form topic, for exampleorders/createX-Shopify-Hmac-Sha256: base64 HMAC-SHA256 of the raw body, keyed with the client secretX-Shopify-Shop-Domain: which store sent it, and so which saved credential verifies itX-Shopify-Webhook-Id: stable across retries, the key to deduplicate onX-Shopify-Event-Id: shared across subscriptions for one merchant actionX-Shopify-API-VersionandX-Shopify-Triggered-At: the payload version, and when the event actually happened rather than when it arrived
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.