StoreFleet
Blog › Shopify Webhooks Basics: Topics, Setup & Integration Guide

Shopify Webhooks Basics: Topics, Setup & Integration Guide

Shopify webhooks basics from an operator running 5 stores — the topics list, shopify.app.toml setup, compliance block, payloads, HMAC, retries.

Linh Nguyen · Updated

Key points — AI summary
  • Compliance topics (customers/data_request, customers/redact, shop/redact) go in their own [[webhooks.subscriptions]] block using compliance_topics, not the normal topics field — mandatory for any App Store app
  • Config-file webhook subscriptions in shopify.app.toml apply app-wide across every shop the app is installed on, and need Shopify CLI 3.63+ to register
  • The products/delete payload contains only the ID — no handle, no title — so without a local product cache keyed by ID, a delete event is unactionable
  • Shopify gives your endpoint 5 seconds to return a 2xx, retries up to 8 times over roughly 4 hours, and auto-deletes a subscription that keeps failing for about 24 hours
  • Verify webhooks by HMAC-SHA256 over the raw request body before any JSON parsing, dedupe on X-Shopify-Webhook-Id, and run a reconciliation job against the Admin API since Shopify doesn't guarantee every event lands

Summarized from this article by our writing pipeline; reviewed by the author.

On this page
  1. What Are Shopify Webhooks?
  2. Shopify Webhook Topics: The Ones You'll Actually Use
  3. Compliance Webhooks: compliance_topics Is Its Own Block
  4. How to Set Up a Shopify Webhook in shopify.app.toml
  5. What the Payloads Actually Look Like
  6. Checkout and Order Payment Webhooks: What Changed in 2026
  7. How Webhook Delivery Works
  8. Verifying Webhook Authenticity
  9. Best Practices for Reliable Webhooks
  10. Why Webhooks Matter for Multi-Store Operations

Everything in our own stack — the order-ops layer, the shipment-tracking lifecycle, the Discord AI agent that answers "where is my order" — stands on a webhook ingestion pipeline I built for the five Shopify stores we operate. It's a Node.js backend that catches events from every store, dedupes them, and writes them to one source of truth. When it works, the whole system feels real-time. When it breaks, I've learned, it breaks quietly.

So this is a Shopify webhooks basics guide written from the endpoint side, not the docs summary. What webhooks are, the topics you'll actually subscribe to, how to register them in shopify.app.toml, what the payloads really contain, and the delivery behavior that has bitten me. Where a detail matters, I link the primary source on shopify.dev so you can verify it against the current API version rather than trust my memory.

What Are Shopify Webhooks?

A webhook is Shopify pushing a notification to your app the instant something happens in a store. You subscribe to a topic — say orders/create — and each time a matching event fires, Shopify sends an HTTP POST to your endpoint with a JSON payload. Your app reacts: sync the order, trigger a workflow, ping a human.

The alternative is polling — asking the Admin API "anything new?" on a timer. Polling burns API quota, adds latency, and scales badly across many stores. Webhooks are event-driven, which is why every serious integration I've built leans on them for the fast path. The caveat, which I'll come back to: fast path is not the same as source of truth.

Shopify Webhook Topics: The Ones You'll Actually Use

Shopify exposes topics across the whole store lifecycle. The authoritative list — with payload samples per topic and per API version — lives in the Shopify webhooks reference on shopify.dev; bookmark that, because topics get added and fields get removed between versions (I'll show two 2026 examples below). These are the ones I actually subscribe to:

Orders, payment & disputes

Customers

Products, variants & inventory

Fulfillment

Compliance — customers/data_request, customers/redact, shop/redact. These are mandatory for App Store apps and, as I explain next, they live in their own configuration block.

Compliance Webhooks: compliance_topics Is Its Own Block

This one trips people up, so it gets its own section. Compliance topics do not go in the topics field of a normal subscription. They go in a separate [[webhooks.subscriptions]] block using the compliance_topics key:

[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
uri = "https://your-app.example.com/webhooks/compliance"

Having multiple [[webhooks.subscriptions]] blocks in one file is fine and expected — one block for your normal topics, a separate one for compliance. These three privacy webhooks are mandatory for any app distributed on the Shopify App Store; the requirements are documented under privacy law compliance on shopify.dev.

Here's why I don't treat this as boilerplate. We run one app installed across all our stores, which means every install carries the same compliance surface. A single missing or broken handler isn't a one-store problem — it's an app-review problem for the whole fleet. When I audit our config, the compliance block gets checked first.

How to Set Up a Shopify Webhook in shopify.app.toml

For most apps, the cleanest way to register webhooks is declaratively in shopify.app.toml. You set the API version once, then add a subscription block per group of topics:

[webhooks]
api_version = "2026-07"

[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "https://your-app.example.com/webhooks/products"

[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "pubsub://your-project:your-topic"

The uri can be an HTTPS endpoint, a Google Pub/Sub URI (pubsub://project:topic), or an Amazon EventBridge ARN. A few options worth knowing:

Two things I wish I'd internalized sooner. First, config-file subscriptions apply across every shop the app is installed on — they're app-level, not per-shop. That's exactly what you want for a fleet, but it means a topic you add is a topic every install now sends you. Second, this declarative flow needs Shopify CLI 3.63 or newer; older CLIs silently won't pick up the subscriptions.

When you need per-shop dynamic subscriptions instead — different topics or endpoints per store, created at runtime — use the GraphQL Admin API webhookSubscriptionCreate mutation rather than the config file. We use the config file for the common baseline and the mutation for anything store-specific.

What the Payloads Actually Look Like

Payloads vary enormously by topic, and assuming they're richer than they are is a classic mistake. I made it.

The products/delete payload is the whole payload:

{ "id": 788032119674292922 }

No handle. No title. No variants. Just the ID. My ingestion assumed a delete event would tell me what was deleted — it doesn't. If you don't keep a local product cache keyed by that ID, the delete event is unactionable; you know something vanished but not what. That lesson cost me a debugging afternoon before I added the cache. Verify it yourself against the sample in the webhooks reference for your API version.

Contrast that with orders/create, which is the opposite problem — it's huge. The payload includes id, admin_graphql_api_id, checkout_token, cart_token, contact_email, currency, current subtotal fields (current_subtotal_price and its _set money-bag variant), the financial totals, and the full line_items array, among many more. You rarely want all of it; this is exactly where include_fields earns its place.

Checkout and Order Payment Webhooks: What Changed in 2026

If you correlate checkouts to orders — abandoned-checkout recovery, attribution — read this before it breaks in production. Per the Shopify changelog, as of 2026-01-30 on API version 2026-04 and later, the id field was removed from checkouts/create and checkouts/update payloads, and checkout_id was removed from seven order webhook topics.

The migration is straightforward once you know: correlate using token on checkout webhooks and checkout_token on order webhooks instead of the old numeric IDs. If your join logic still keys on checkout_id, it goes quietly null the moment you bump API versions.

For payments and refunds, order_transactions/create remains the webhook to watch. And don't confuse any of this with checkout_and_accounts_configurations/update, a different topic that was removed entirely as of 2026-01-01 — it's gone, not renamed.

How Webhook Delivery Works

Register a subscription and you're specifying a topic, a destination (HTTPS, Google Pub/Sub, or EventBridge), a format (JSON, the default), and optional filters. Then delivery works like this: an event fires, Shopify POSTs a JSON payload to your endpoint, and your endpoint has 5 seconds to return a 2xx. Miss that — timeout, 5xx, network failure — and Shopify retries up to 8 times over roughly 4 hours with exponential backoff.

Two behaviors here matter more than the happy path.

First, a subscription that keeps failing gets auto-deleted. Per the webhook troubleshooting docs, if your endpoint fails delivery repeatedly over about 24 hours, Shopify removes the subscription. This is the failure mode that scares me most on our stack: an endpoint that 500s all night doesn't just drop events, it silently unsubscribes you, and the next morning everything looks calm because nothing is even trying to deliver anymore. Alerting on delivery-failure rate is not optional.

Second, a retried delivery carries the original payload from event time, not a refreshed snapshot. So by the time a retry lands, the data can be stale relative to the store. Use the X-Shopify-Triggered-At header to know when the event actually happened and to detect that you're processing history, not the present.

Verifying Webhook Authenticity

Every legitimate delivery to an HTTPS endpoint carries headers that prove it came from Shopify (Pub/Sub and EventBridge deliveries authenticate through the cloud platform itself instead):

To verify, compute an HMAC-SHA256 of the request body with your app secret and compare it to the header. The gotcha that cost me real time: HMAC is computed over the raw request body. If your framework parses JSON before you verify — express.json() running as global middleware, for instance — the bytes you hash no longer match the bytes Shopify signed, and every verification fails even though nothing is actually wrong. Capture the raw body first, verify, then parse:

const digest = crypto
  .createHmac('sha256', SHOPIFY_API_SECRET)
  .update(rawBody) // raw bytes, before JSON parsing
  .digest('base64');

Shopify's official libraries wrap this for you via shopify.webhooks.validate(), and following the verify-deliveries guidance is the safest path. Separately, use X-Shopify-Webhook-Id to dedupe — if you've processed that ID before, drop the duplicate. On our pipeline that ID is the idempotency key that makes the "retried up to 8 times" behavior harmless.

Best Practices for Reliable Webhooks

Five habits that hold up in production:

  1. Reconcile — don't trust webhooks as the source of truth. Shopify doesn't guarantee every event lands. Our reconciliation job queries the Admin API by updated_at on a schedule and backfills anything the webhook stream missed — and it does catch gaps, especially after an outage or an auto-unsubscribe. Webhooks are the fast path; the reconciliation job is what makes the data correct.
  2. Assume events arrive out of order. Different topics aren't chronologically synchronized. Reconstruct sequence from updated_at or X-Shopify-Triggered-At, never from arrival order.
  3. Return 2xx fast, process async. With a 5-second timeout, do the minimum inline — verify, enqueue, respond — and let a background worker (Redis, a job queue) do the heavy lifting.
  4. Filter at the source. filter and include_fields cut payload size and handler load before the data ever reaches you.
  5. Log every delivery. Payload, headers, processing result. When an integration misbehaves, these logs are the only thing that tells you whether the event never arrived or arrived and was mishandled.

Why Webhooks Matter for Multi-Store Operations

One store, and webhooks are a convenience. Five stores, and they're the only sane architecture. Polling five stores multiplies your API load and your lag; a webhook pipeline lets each store push to the same ingestion layer the instant something changes.

That's the pattern to run on. Webhooks from every store land in one backend, get deduped and reconciled, and populate one unified dashboard — so you're managing multiple Shopify stores from a single view instead of five admin tabs. On top of that data layer sit the pieces this pipeline enables: order sync to Google Sheets, bulk shipment tracking via 17TRACK, stuck-shipment alerts, consolidated finance across stores, and store-health monitoring and alerting. If you'd rather drive execution than just observe it, the Shopify Bot API automation layer acts on these events, and there's a full breakdown of Shopify Flow vs Zapier vs a custom bot if you're deciding where your logic should live.

Whether you run 5 stores or 50, a single webhook-driven integration replaces dozens of disconnected workflows.