An order comes through on a Shopify store. Somewhere else, an ERP, a 3PL’s warehouse system, a CRM, a second sales channel, something needs to know about it within seconds, not the next time a scheduled job happens to run. That’s the problem Shopify webhooks solve, and it’s also where a lot of otherwise solid integrations quietly fall over: a missed retry, an unverified payload, a duplicate order created because the same event arrived twice.
This post explains what a Shopify webhook actually is, how event-driven delivery differs from polling the Admin API, how to register webhooks and secure them properly, and how Shopify’s retry behaviour works so you can build a sync that stays accurate under real trading conditions, not just in a quiet test store.
If you’re currently polling the API on a timer to “check for changes,” or you’ve had orders go missing from a connected system after a busy sales period, this is almost certainly a webhook reliability problem, and it’s a well-understood one to fix.
What a Shopify Webhook Actually Is
A webhook is an event-driven HTTP callback. When something happens in a Shopify store, an order is created, a product is updated, an inventory level changes, Shopify sends an HTTP POST request containing details of that event to a URL you’ve registered in advance. Your endpoint receives the payload, processes it, and returns a response. No request from your side triggers it; Shopify pushes the data to you the moment the event occurs.
This is fundamentally different from polling, where your system periodically calls the Admin API (REST or GraphQL) and asks, in effect, “has anything changed since I last checked?” Polling works, but it has three structural weaknesses:
- Latency, you only find out about a change at your next poll interval, not when it actually happens.
- Wasted requests, most polls return “nothing changed,” which still consumes API rate limit budget for no benefit.
- Missed changes between polls, if two updates happen to the same record between polls, you may only ever see the latest state, not the sequence of events.
Webhooks solve all three: you’re notified the moment something happens, you’re not burning API calls checking for nothing, and (with correct handling) you see every discrete event rather than just a snapshot.
How to Register a Shopify Webhook
There are two practical ways to set up webhooks on a Shopify store, depending on whether you’re configuring something ad hoc or building a proper integration.
Option 1: Shopify admin (Settings > Notifications)
For simple cases, a single endpoint that needs to know about one or two event types, you can register a webhook manually from the Shopify admin under Settings > Notifications, scrolling to the Webhooks section. You select an event topic, choose a payload format (JSON is standard), enter your endpoint URL, and save. This creates a webhook subscription tied to that store.
This method is fine for lightweight, one-off needs, but it doesn’t scale well: it’s manual, per-store, and easy to forget when you’re managing subscriptions across several client stores or need to programmatically manage which topics are active.
Option 2: Admin API or app (the standard approach for real integrations)
For anything beyond a one-off, webhook subscriptions are created programmatically, either via the Admin API’s webhookSubscription mutations (GraphQL) or the equivalent REST endpoint, or automatically as part of an app’s installation flow if you’re building a Shopify app. This is the standard approach for integration work because it’s:
- Repeatable, the same registration logic runs on every store you deploy to.
- Auditable, you can query which webhooks are currently registered and their delivery status.
- Tied to app lifecycle, if you’re building a public or custom app, Shopify expects you to declare webhook subscriptions in your app configuration, and subscriptions are automatically cleaned up if the app is uninstalled.
For most merchant integrations, syncing to an ERP, a 3PL, or a CRM, this happens through a custom or private app with the correct API scopes granted, registering webhooks as part of that app’s setup rather than manually in the admin.
Common Webhook Topics You’ll Actually Use
Shopify exposes a large number of webhook topics, but a handful cover the vast majority of real integration work:
orders/create, fires when a new order is placed. The starting point for syncing orders into an ERP, accounting system, or fulfilment platform.orders/updated, fires when an existing order changes (payment status, line items, shipping details). Essential if you’re not just capturing new orders but keeping their downstream record current.orders/fulfilledandorders/cancelled, useful for triggering downstream status changes, such as marking an order as shipped in a CRM or releasing reserved stock after a cancellation.inventory_levels/update, fires when stock levels change at a location. The backbone of any multi-channel inventory sync, where Shopify needs to reflect (or be informed by) stock levels held elsewhere.products/update, fires when a product or its variants change. Used to keep a PIM, marketplace listing, or secondary catalogue aligned with the source of truth in Shopify.app/uninstalled, fires when a merchant uninstalls your app. Critical for cleanup: revoking stored access tokens, pausing syncs, and not continuing to process a store you no longer have a relationship with.
Choosing the right, minimal set of topics for a given integration matters more than it sounds. Subscribing to more topics than you actually act on just creates unnecessary load on your endpoint and more payloads to filter through.
Verifying Webhooks with HMAC Signatures
Because a webhook endpoint is a public URL, anyone who discovers it could, in theory, send it a fake payload pretending to be Shopify. Shopify addresses this by signing every webhook request with an HMAC-SHA256 signature, sent in the X-Shopify-Hmac-SHA256 header.
To verify a webhook is genuinely from Shopify:
- Take the raw request body (before any JSON parsing, this matters, as re-serialising the payload can change the byte content and break the signature check).
- Compute an HMAC-SHA256 hash of that raw body using your app’s client secret as the key.
- Base64-encode the result and compare it, using a constant-time comparison, against the value in the
X-Shopify-Hmac-SHA256header. - If they don’t match, reject the request, don’t process it and don’t return a success response.
This step is not optional for anything touching orders, customer data, or inventory. Skipping HMAC verification means your endpoint will process any correctly-formatted POST request sent to it, from anyone, as if it were a genuine Shopify event.
Shopify’s Retry Behaviour and Exponential Backoff
Webhook delivery isn’t guaranteed on the first attempt, your endpoint might be down for a deployment, slow to respond, or briefly overloaded. Shopify handles this with a retry mechanism: if your endpoint doesn’t respond with a 200 OK (or times out), Shopify retries the delivery, backing off with increasing delay between attempts over a set retry window, before eventually giving up on that specific delivery.
The practical implications for how you build your endpoint:
- Respond fast, process async. Your endpoint should validate the HMAC signature, acknowledge receipt with a
200response, and hand the actual processing (writing to your ERP, updating inventory elsewhere) off to a queue or background job. If your endpoint does slow, synchronous work before responding, you risk Shopify timing out and retrying a webhook you already received, creating duplicate processing. - Assume some deliveries will be retries. Because of this exact mechanism, any webhook consumer should expect to occasionally receive the same event more than once and needs a plan for it (see idempotency, below).
- Don’t build your own retry logic on top for outbound calls without backoff. If your webhook handler then calls out to a third-party API (the ERP, the 3PL), and that call fails, apply the same principle back the other way, retry with exponential backoff rather than hammering the downstream system immediately.
Idempotency: Handling Duplicate Deliveries
Because retries exist, and because network conditions can occasionally cause a delivery to be sent more than once even without an explicit failure, a well-built webhook handler must be idempotent, processing the same event twice should not create a duplicate order, double-adjust inventory, or send a duplicate notification.
The standard pattern:
- Every Shopify webhook payload includes the resource’s ID (for example, the order ID) and, in most cases, an
X-Shopify-Webhook-Idheader unique to that specific delivery. - Before processing, check whether you’ve already handled that ID (a simple “processed events” table or cache keyed on the webhook ID or resource ID plus a version/updated-at value works well).
- If it’s already been processed, acknowledge with a
200and skip the processing step, don’t treat a duplicate as an error, and don’t silently reprocess it either. - For updates specifically, compare a timestamp or version field rather than blindly overwriting, so an out-of-order delivery doesn’t stomp a more recent state with an older one.
This is one of the most commonly skipped steps in DIY webhook integrations, and it’s usually the root cause when a merchant reports “sometimes we get the same order twice in our system.”
Webhooks vs Polling: When Each Makes Sense
Webhooks are the right default for anything that needs near-real-time sync, orders, inventory, fulfilment status. But polling still has a place:
- Polling makes sense for periodic reconciliation (a nightly job that compares full state between Shopify and a downstream system to catch anything a webhook might have missed), for bulk historical backfills, or for data that doesn’t have a corresponding webhook topic.
- Webhooks make sense for anything time-sensitive, high-frequency, or where API rate limit budget is a genuine constraint, which, for order and inventory sync specifically, is almost always the case.
In practice, the most reliable integrations use both: webhooks for real-time updates, plus a periodic reconciliation job as a safety net in case a delivery was ever missed entirely (which, while rare, can happen, an endpoint down for an extended outage, for instance).
A Practical Checklist for a Reliable Webhook Integration
- [ ] Register only the webhook topics you’ll actually act on, via the Admin API or app configuration rather than manual admin setup, for anything beyond a one-off.
- [ ] Verify the
X-Shopify-Hmac-SHA256signature on every request against the raw request body, and reject anything that doesn’t match. - [ ] Return a
200response quickly, and move actual processing (ERP writes, inventory updates) into an async job or queue. - [ ] Track processed webhook IDs or resource versions to make handling idempotent, duplicates should be safely ignored, not reprocessed.
- [ ] Handle
app/uninstalledexplicitly, cleaning up stored tokens and pausing syncs for that store. - [ ] Add a periodic reconciliation job as a safety net for the rare missed or expired delivery.
- [ ] Log every webhook received, including duplicates and failures, so you can diagnose sync gaps after the fact rather than guessing.
Real-World Use Cases This Pattern Supports
Once the fundamentals above are in place, webhooks become the backbone of most Shopify integration work:
- Syncing orders to an ERP,
orders/createandorders/updatedpush new and changed orders into an accounting or ERP system in near real time, rather than the ERP being hours behind. - Syncing inventory across channels,
inventory_levels/updatekeeps stock accurate across a website, a marketplace listing, and a physical POS, reducing the risk of overselling. - Triggering fulfilment in a 3PL system,
orders/create(often filtered by fulfilment status or tags) can trigger a pick-pack-ship request in a third-party logistics platform the moment an order is ready. - Updating a CRM, order and customer events keep a CRM’s record of purchase history and lifecycle stage current without a separate manual export process.
When to Bring in a Specialist
Building a single webhook handler for a simple use case is achievable in-house. Where it gets genuinely harder is at scale: multiple webhook topics feeding into a system that also needs idempotency handling, queueing, retry logic on outbound calls, monitoring for missed deliveries, and correct handling of edge cases like out-of-order updates or partial failures mid-sync. Getting the architecture right up front, rather than patching duplicate-order bugs after they’ve already reached a customer or an accounts team, is exactly the kind of work our Shopify integrations team handles regularly, whether that’s an ERP sync, a 3PL connection, or a custom middleware layer between Shopify and the rest of your stack.
Frequently Asked Questions
What’s the difference between a Shopify webhook and the Admin API?
The Admin API is something you call to request data on demand, you ask, Shopify answers. A webhook is the reverse: Shopify calls your endpoint the moment a relevant event happens, without you asking. Most real integrations use both, webhooks for real-time event notification, and the Admin API to fetch additional detail once a webhook tells you something changed.
Why is my webhook endpoint receiving the same event more than once?
This is expected behaviour, not a bug on Shopify’s side. If your endpoint doesn’t respond in time or returns an error, Shopify retries delivery, which can result in the same event arriving more than once. Your endpoint needs to be idempotent, tracking which webhook IDs or resource updates it has already processed, so duplicates don’t cause duplicate orders or incorrect inventory adjustments.
Do I need a public HTTPS endpoint to receive Shopify webhooks?
Yes. Shopify delivers webhooks as HTTP POST requests to a publicly reachable URL over HTTPS. For local development, this typically means using a tunnelling tool to expose your local environment temporarily; for production, it means a properly hosted, secured endpoint.
How long does Shopify keep retrying a failed webhook delivery?
Shopify retries a failed delivery with increasing delay between attempts for a period before giving up on that specific event. Exact retry windows and attempt counts can change between API versions, so if reliability is critical, don’t rely solely on Shopify’s retries, build a periodic reconciliation job as a backup so a rare missed delivery doesn’t silently leave two systems out of sync.
Can I use webhooks instead of polling for everything?
Mostly, but not entirely. Webhooks cover discrete events well, but they’re not designed for bulk historical data pulls or full-catalogue reconciliation. A well-built integration typically uses webhooks for real-time updates and a scheduled reconciliation job for periodic full-state checks, rather than relying on either approach alone.
Get Your Webhook Architecture Reviewed
If you’re relying on manual polling, seeing duplicate orders in a connected system, or planning a new integration that needs to stay accurate under real order volume, it’s worth having the webhook design checked before it’s carrying production traffic. Book a call with our team and we’ll walk through your specific systems and sync requirements.