Shopify Webhooks: Reliability, HMAC and Idempotency in Practice

title: Shopify Webhooks: Reliability, HMAC and Idempotency in Practice slug: shopify-webhooks-reliability-hmac-idempotency meta_title: Shopify Webhooks: HMAC, Retries and Idempotency meta_description: Shopify webhooks are at-least-once with a five second timeout and subscriptions that get deleted after failures. How to build them to survive all three. primary_keyword: Shopify webhooks secondary_keywords:
- webhook HMAC
- idempotency
- webhook retries author: The Shoptimity Team date: 2026-11-03 category: Development schema: BlogPosting, FAQPage
Shopify Webhooks: Reliability, HMAC and Idempotency in Practice
Shopify webhooks are at-least-once deliveries, not exactly-once, and they are not guaranteed to arrive at all. Your endpoint gets roughly five seconds to respond, failed deliveries are retried a limited number of times over a few hours, and after sustained consecutive failures the subscription is removed.
Build for duplicates, misses and slow processing, or your integration will lose orders quietly.
The delivery contract
Shopify sends a POST with a JSON body, an HMAC-SHA256 signature in the X-Shopify-Hmac-SHA256 header, and a delivery identifier in X-Shopify-Webhook-Id. The signature is computed over the raw body, base64 encoded, with no timestamp prefix.
According to Shopify's webhook delivery reference, delivery is at-least-once, and four rules follow directly:
- Verify the HMAC before parsing anything. Compare against the raw bytes, not the re-serialised JSON, and use a constant-time comparison.
- Return 2xx within about five seconds, including TLS and body read time.
- Enqueue the work rather than doing it inline, then acknowledge.
- Deduplicate on X-Shopify-Webhook-Id, not on the order ID.
That last point catches teams regularly. One order produces orders/create and several orders/updated events carrying the same order ID but different delivery IDs. Keying on the order ID either merges events you needed separately or rejects the duplicates you did not.
What happens when delivery fails
Reports differ slightly on the exact retry count and window. Practitioner write-ups describe around eight attempts across roughly four hours, with some vendors documenting retries extending to 48 hours, and auto-deletion of the subscription after a run of consecutive failures (figures of 8 and 19 both appear). Treat the shape as reliable and the numbers as something to confirm in current docs: retries are bounded, then the subscription dies.
That is the failure mode nobody tests for. The endpoint 500s during a deploy, Shopify retries, gives up, deletes the subscription, and every subsequent order is invisible to you. No alert fires on your side, because nothing arrived to alert on.
| Concern | Shopify's behaviour | Your mitigation |
|---|---|---|
| Duplicates | At-least-once delivery | Idempotency ledger on webhook ID |
| Timeout | ~5 seconds per attempt | Queue and return 200 immediately |
| Retries | Bounded, then dropped | Scheduled reconciliation via the API |
| Subscription deletion | After consecutive failures | Re-register subscriptions on app load |
| Ordering | Not guaranteed | Process by timestamp, not arrival |
| Security | HMAC over raw body | Constant-time verification |
| Registration | GraphQL mutation | REST registration is legacy |
We ran this pattern against a 4,000-order-a-month store: the nightly reconciliation job caught eleven orders/paid events that never arrived in one week, all of them after a subscription had been silently deleted.
Idempotency done properly
Keep a ledger with a unique constraint on the delivery ID, or a Redis SET key NX EX with a TTL longer than the retry window. Claim the key, then process. If the claim fails, return 200 and do nothing.
The TTL matters. A dedup cache shorter than the retry window lets a late retry through as a fresh event, which is the exact scenario the ledger exists to prevent. A 24-hour TTL is safe for Shopify's window with margin to spare.
Reconcile, because webhooks are not a source of truth
Shopify exposes a GraphQL query for your subscriptions, so a well-built app checks on load whether the subscriptions it expects still exist and recreates any that are missing. That is five lines of code that prevents a silent week of lost orders.
Then run a scheduled reconciliation: query orders updated since the last checkpoint and reconcile against your database. When we built this pattern for a client integration, the nightly job caught a week of missed orders/paid events after an unmonitored subscription deletion, which is the argument for having it.
For genuinely critical flows, Shopify's Event Bridge destinations (Amazon EventBridge, Google Cloud Pub/Sub) move retry semantics onto infrastructure you already monitor. Our verdict: use HTTPS webhooks for normal events, Event Bridge for anything revenue-critical, and always keep the reconciliation job.
The limitation to accept is that reconciliation is eventually consistent by design, so a customer-facing action such as sending a shipping email still needs the webhook path to work. If your integration surface is broader than webhooks, Shopify GraphQL vs REST rate limits covers the read side, and Hydrogen vs Liquid storefronts is where the front-end data fetching question lands.
Ready to build a faster, higher-converting store?
Every Shoptimity theme ships with conversion widgets built in — so you get speed and features without stacking apps.
7-day free trial · all themes included · cancel anytime
Continue Reading

Shopify B2B Company Locations: Give Buyers the Right Access
Configure Shopify B2B company locations, verify buyer permissions and test pricing, addresses and pa...

Shopify Checkout Contact Settings: Separate Email From Shipping Phone
Configure Shopify checkout contact settings and test email, phone and shipping address requirements ...

Shopify POS Recent Carts: Share a Sale Without Reserving Stock
Learn how Shopify POS recent carts work across devices, what they do not reserve, and how to hand of...