Talk Shop
Home
Learn More
About Us
Follow Us
Blog
Tools
Newsletter
Join Discord
Join

Community

  • Developers
  • Growth
  • Entrepreneurs
  • Support
  • Experts
  • Tools

Location

123 Mars, Crater City, Red Planet

(WiFi may be spotty)

Hours

Who has time for breaks? We're here 24/7!

Contact

hello@letstalkshop.com

Talk Shop
Talk Shop

Built for real builders. Not affiliated with Shopify Inc.

Home
Privacy
Terms
  1. Home
  2. >Blog
  3. >Shopify Development
  4. >Shopify Webhooks: The Complete Developer Guide (2026)
Shopify Development14 min read

Shopify Webhooks: The Complete Developer Guide (2026)

Master Shopify webhooks end to end: declarative TOML subscriptions, HMAC verification in Node, retry behavior, idempotency, EventBridge and Pub/Sub at scale, compliance topics, and reconciliation strategies that survive missed deliveries.

Talk Shop

Talk Shop

Jun 11, 2026

Shopify Webhooks: The Complete Developer Guide (2026)

In this article

  • Why Polling Is the Wrong Way to Watch a Shopify Store
  • How Shopify Webhooks Actually Work
  • Two Ways to Subscribe to Shopify Webhooks
  • Verifying Deliveries with HMAC Signatures
  • At-Least-Once Delivery: Retries, Timeouts, and Ordering
  • Building Idempotent Shopify Webhook Handlers
  • Scaling Up: Amazon EventBridge and Google Pub/Sub
  • The Mandatory Compliance Webhooks
  • When Webhooks Go Missing: Failure Modes and Reconciliation
  • Testing Shopify Webhooks Locally
  • Ship Webhooks That Survive the Real World

Why Polling Is the Wrong Way to Watch a Shopify Store

How does your app find out an order was just placed? If the answer is "we hit the API every 60 seconds and diff the results," you're burning rate limit budget to learn things Shopify would have told you for free.

Shopify webhooks flip the model. Instead of your app asking "anything new?" thousands of times a day, Shopify sends an HTTPS POST to your endpoint the moment an event happens — an order is created, a product is updated, an app is uninstalled. You get near-real-time data, you stop wasting GraphQL rate limit headroom on empty polls, and your app reacts in seconds instead of minutes.

But webhooks come with sharp edges that don't show up in tutorials: deliveries arrive more than once, sometimes out of order, and occasionally not at all. Subscriptions can silently vanish after repeated failures. And every public App Store app must handle three mandatory privacy webhooks before review. This guide covers the full lifecycle — subscribing, verifying, retrying, scaling, and reconciling — with everything checked against current Shopify documentation. Webhooks are one slice of the platform's eventing surface; for the request/response side, our complete guide to the Shopify Admin API is the companion read.

How Shopify Webhooks Actually Work

Two dark metallic data pipelines, one complex and flickering, the other glowing electric blue.

A Shopify webhook subscription is a simple contract: you tell Shopify which topic you care about and where to send deliveries. When a matching event fires in a shop, Shopify serializes the resource (as JSON or XML) and POSTs it to your destination.

The Anatomy of a Delivery

Every delivery has three parts you'll work with constantly:

  • Topic — the event type, named as resource/event: orders/create, products/update, app/uninstalled, inventory_levels/update, and dozens more
  • Payload — the resource itself, e.g. the full order object for orders/create
  • Headers — metadata that tells you what happened, when, and whether the request is genuine

Shopify's official webhooks documentation frames the system around four jobs: manage subscriptions, filter deliveries, understand the delivery structure, and verify each delivery. That's a useful mental model — and it's exactly how this guide is organized.

The Headers That Matter

Four headers do most of the heavy lifting in production handlers:

HeaderWhat it's for
X-Shopify-TopicIdentifies the event, so one endpoint can route many topics
X-Shopify-Hmac-Sha256Base64 HMAC signature — proves the request came from Shopify
X-Shopify-Webhook-IdUnique delivery ID — your key for duplicate detection
X-Shopify-Triggered-AtWhen the event actually occurred (not when it was delivered)

Bookmark that table. Signature verification and idempotency — the two things that separate toy handlers from production ones — both live in these headers.

Trimming Payloads with Filters and include_fields

You rarely need the whole resource. Two subscription-level options keep noise down:

  • `filter` — only deliver events matching a condition (e.g., only orders above a certain total), so your endpoint never sees irrelevant events
  • `include_fields` — only include the named fields in the payload, shrinking payload size and reducing the blast radius of any logging mistakes

Both are set per subscription, in either of the two subscription methods covered next.

Two Ways to Subscribe to Shopify Webhooks

Modern Shopify development gives you two subscription mechanisms for Shopify webhooks, and choosing correctly up front saves real pain later. (If you're coming from the REST /admin/webhooks endpoints: those still exist, but the REST Admin API is legacy — new apps should use the declarative config or GraphQL.)

App-Specific Subscriptions in shopify.app.toml

The recommended default is declarative configuration in your app's shopify.app.toml. Subscriptions defined here apply identically to every shop that installs your app, and they sync automatically when you run shopify app deploy:

tomltoml
[webhooks]
api_version = "2026-04"

[[webhooks.subscriptions]]
topics = ["orders/create", "orders/updated"]
uri = "https://your-app.example.com/webhooks/orders"

[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "https://your-app.example.com/webhooks/products"
include_fields = ["id", "title", "updated_at", "variants"]

This is infrastructure-as-code for your event layer: subscriptions live in version control, code review sees every change, and a deploy reconciles Shopify's state with your file. Shopify explicitly recommends this approach unless your configuration must differ per shop.

Shop-Specific Subscriptions via the GraphQL Admin API

When subscriptions need to vary per installation — a merchant toggles a feature that requires inventory_levels/update, for example — create them at runtime with the webhookSubscriptionCreate mutation:

graphqlgraphql
mutation {
  webhookSubscriptionCreate(
    topic: ORDERS_CREATE
    webhookSubscription: {
      uri: "https://your-app.example.com/webhooks/orders"
      format: JSON
    }
  ) {
    webhookSubscription {
      id
      topic
      uri
    }
    userErrors {
      field
      message
    }
  }
}

One modernization worth knowing: the old transport-specific mutations like eventBridgeWebhookSubscriptionCreate are deprecated. The unified webhookSubscriptionCreate now accepts any destination through its uri field — an HTTPS URL, a pubsub://{project-id}:{topic-id} URI, or an Amazon EventBridge ARN. One mutation, three transports.

Which Should You Use?

SituationUse
Same topics and endpoint for every shopshopify.app.toml (declarative)
Topics or destinations vary per shopGraphQL webhookSubscriptionCreate
Mandatory compliance topicsshopify.app.toml (compliance_topics)
Legacy REST /admin/webhooks codeMigrate to one of the above

Most apps end up hybrid: a declarative baseline for universal topics, plus GraphQL for anything merchant-configurable. If you're still scaffolding your project, our walkthrough of Shopify app development from first principles covers where webhook handlers fit in a Remix app template.

Verifying Deliveries with HMAC Signatures

Your webhook endpoint is a public URL that mutates your database. Anyone who discovers it can POST fake orders unless you verify every request — which is why Shopify signs each delivery and why verification is non-negotiable.

How the Signature Works

Shopify computes an HMAC-SHA256 digest of the raw request body using your app's client secret as the key, base64-encodes it, and sends it in the X-Shopify-Hmac-Sha256 header. You repeat the computation server-side and compare. Per Shopify's verification documentation, any delivery where the signatures don't match must be rejected.

A Working Node.js Verifier

Here's the pattern in Express, with the two details most implementations get wrong handled correctly:

javascriptjavascript
const crypto = require("crypto");
const express = require("express");

const app = express();
const CLIENT_SECRET = process.env.SHOPIFY_CLIENT_SECRET;

// CRITICAL: capture the RAW body — express.json() would consume it first
app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const hmacHeader = req.get("X-Shopify-Hmac-Sha256");
    if (!hmacHeader) return res.sendStatus(401);

    const digest = crypto
      .createHmac("sha256", CLIENT_SECRET)
      .update(req.body) // raw Buffer, NOT a parsed object
      .digest("base64");

    const valid = crypto.timingSafeEqual(
      Buffer.from(digest, "base64"),
      Buffer.from(hmacHeader, "base64")
    );
    if (!valid) return res.sendStatus(401);

    // Acknowledge immediately; process async (see retry section)
    res.sendStatus(200);

    const topic = req.get("X-Shopify-Topic");
    const payload = JSON.parse(req.body.toString("utf8"));
    queueForProcessing(topic, payload, req.get("X-Shopify-Webhook-Id"));
  }
);

If you're using Shopify's official libraries (@shopify/shopify-api, or the Remix app template's authenticate.webhook(request)), verification is built in — but you should still understand what it's doing, because the failure mode of a misconfigured verifier is every webhook silently rejected.

The Two Classic Verification Bugs

  1. Parsed body instead of raw body. If express.json() (or any framework body parser) runs first, you'll re-serialize the object and the bytes won't match — verification fails on every legitimate request. Mount express.raw() on the webhook route specifically.
  2. String comparison instead of timing-safe comparison. digest === header leaks timing information an attacker can exploit. Use crypto.timingSafeEqual on equal-length buffers, as above.

And one operational note: respond 401 to bad signatures, log them, and move on. Don't retry-process a request you couldn't authenticate.

At-Least-Once Delivery: Retries, Timeouts, and Ordering

A floating smartphone and tablet showing dark-themed JSON payloads and headers with electric blue lighting.

Shopify webhooks are at-least-once, not exactly-once, and not ordered. Designing for those three words is most of webhook engineering.

The Delivery Contract

  • Your endpoint must respond with a 200-range status. Anything else — including 3XX redirects — counts as a failure.
  • Shopify enforces a 1-second connection timeout and a 5-second timeout for the entire request. Slow handlers are failed handlers.
  • On failure, Shopify retries 8 times over the next 4 hours with increasing backoff.
  • After repeated consecutive failures, Shopify emails your app's emergency developer address — and subscriptions created through the Admin API are automatically deleted. Declarative TOML subscriptions are restored on your next deploy, which is one more argument for them.

That 5-second ceiling drives the cardinal architecture rule: acknowledge first, process later. Verify the HMAC, persist or enqueue the payload, return 200, and do the real work — inventory sync, email triggers, order tagging and fulfillment automation — in a background worker. Hookdeck's getting-started guide to Shopify webhooks walks through the same queue-first pattern with worked examples.

Why Duplicates Happen

Suppose your handler finishes processing in 5.2 seconds. The work succeeded — but Shopify timed out at 5.0 and recorded a failure. Minutes later, the retry arrives and you process the same order twice. Network blips, deploys, and cold starts produce the same effect. Duplicates aren't an edge case; they're the steady-state behavior of any at-least-once system.

Ordering Isn't Guaranteed Either

Shopify does not guarantee event ordering within or across topics. An orders/updated can arrive before the orders/create it logically follows. Two defenses:

  • Compare timestamps, don't assume sequence. Use X-Shopify-Triggered-At (or updated_at in the payload) and ignore events older than the state you've already stored.
  • Upsert, don't insert-then-update. If orders/updated arrives first, an upsert creates the record; the late orders/create becomes a no-op.

Building Idempotent Shopify Webhook Handlers

Idempotency means processing the same delivery twice produces the same result as processing it once. There are two complementary strategies — most production apps use both.

Strategy 1: Deduplicate on X-Shopify-Webhook-Id

Every delivery carries a unique X-Shopify-Webhook-Id. Persist it before processing:

javascriptjavascript
async function handleDelivery(webhookId, topic, payload) {
  const inserted = await db.query(
    `INSERT INTO processed_webhooks (webhook_id)
     VALUES ($1)
     ON CONFLICT (webhook_id) DO NOTHING
     RETURNING webhook_id`,
    [webhookId]
  );
  if (inserted.rowCount === 0) return; // duplicate — already handled

  await processEvent(topic, payload);
}

The INSERT ... ON CONFLICT DO NOTHING makes the check-and-claim atomic, so two concurrent duplicate deliveries can't both pass. Prune rows older than a few days — retries stop after 4 hours, so a 7-day window is generous.

Strategy 2: Make the Operations Themselves Idempotent

ID-based dedupe protects against identical deliveries; it doesn't protect against two different events that touch the same data. Idempotent operations do:

  • Upserts keyed on Shopify's resource ID (order.id, product.id) instead of blind inserts
  • State checks before side effects — "send welcome email if not already sent," recorded in your DB, not in memory
  • Absolute writes over relative ones — set inventory = payload.available, never inventory += delta

The Queue-First Reference Architecture

Put the pieces together and the shape is always the same:

  1. Endpoint verifies HMAC → 2. claims X-Shopify-Webhook-Id → 3. enqueues {topic, payload, webhookId} → 4. returns 200 in milliseconds → 5. worker processes with idempotent operations and its own retry policy.

This keeps you under the 5-second timeout at any traffic level, survives worker crashes, and absorbs flash-sale bursts gracefully.

Scaling Up: Amazon EventBridge and Google Pub/Sub

An isometric 3D visualization of a glowing electric blue idempotent processing system on a dark background.

Past a certain volume of Shopify webhooks, running your own HTTPS ingestion tier becomes the bottleneck: every flash sale is a self-inflicted DDoS against your own endpoint. Shopify supports two managed alternatives that remove your servers from the delivery path entirely.

Amazon EventBridge

Shopify acts as a partner event source for Amazon EventBridge: deliveries land on an event bus in your AWS account, and rules route them to Lambda, SQS, Step Functions, or anywhere else. Subscribe by setting the subscription uri to your EventBridge ARN. Shopify's delivery problem becomes AWS's scaling problem — which AWS is rather good at.

Google Pub/Sub

The GCP equivalent uses Google Cloud Pub/Sub as a message bus: grant Shopify's service account publish rights on a topic, then subscribe with uri = "pubsub://{project-id}:{topic-id}". Your consumers pull at their own pace with Pub/Sub's own ack/redelivery semantics, so a burst of 10,000 order events queues calmly instead of hammering an endpoint.

When to Switch

SignalRecommendation
Low volume, simple appHTTPS is fine — don't add infrastructure
Timeouts during traffic spikesMove hot topics (orders, inventory) to EventBridge/Pub/Sub
Already on AWS or GCPUse the matching transport from day one
Multiple consumers per eventMessage bus fan-out beats HTTPS fan-out

Note that HMAC verification is an HTTPS concern; with EventBridge and Pub/Sub, authenticity comes from the cloud provider's IAM trust between Shopify and your account.

The Mandatory Compliance Webhooks

This section isn't optional reading: any app distributed through the Shopify App Store must respond to data subject requests, whether or not it collects personal data. App review checks for it.

The Three Topics

TopicTriggerYour obligation
customers/data_requestA customer asks to view their stored dataProvide the requested data to the store owner
customers/redactA store owner requests deletion of a customer's dataErase that customer's personal data
shop/redactFires 48 hours after your app is uninstalledErase the shop's data

Configuring Them

Compliance topics are declared in shopify.app.toml with the dedicated compliance_topics key:

tomltoml
[webhooks]
api_version = "2026-04"

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

The Response Rules Are Stricter Here

The response rules for these Shopify webhooks are stricter than for ordinary topics. Three requirements, verified during app review:

  1. Confirm receipt with a 200-series status code
  2. Return 401 Unauthorized when the HMAC header is invalid — for compliance topics this is an explicit requirement, not just best practice
  3. Complete the requested action within 30 days (unless you're legally required to retain the data)

Even an app that stores zero customer data must subscribe and return 200. Build the no-op handler; it's ten minutes of work that unblocks your App Store listing.

When Webhooks Go Missing: Failure Modes and Reconciliation

A minimalist, dark office environment at night showing three server racks connected by a glowing cyan cable.

Here's the line from Shopify's own docs that should shape your architecture: webhook delivery isn't always guaranteed. Treat Shopify webhooks as an optimization for freshness, not as your source of truth.

The Failure Modes You'll Actually Hit

  • Dropped deliveries — after 8 failed retries over 4 hours, that delivery is gone for good
  • Silent subscription removal — repeated consecutive failures delete API-created subscriptions; if you miss the warning email, your app keeps running while hearing nothing
  • Deploy-window outages — a bad release returns 500s for an hour; every event in that window exhausts retries against a broken endpoint
  • The uninstall gap — app/uninstalled can fail like any other delivery, leaving you syncing against a shop that revoked your token weeks ago

Reconciliation: The Safety Net

A reconciliation job is a scheduled sweep that asks the Admin API what webhooks should have told you:

  1. Poll on a slow cadence — e.g., hourly, query orders(query: "updated_at:>{last_sync}") and upsert anything your webhook path missed
  2. Audit your subscriptions — periodically run the webhookSubscriptions query and alert if expected topics are missing (this catches silent removal)
  3. Heartbeat your topics — if a normally-chatty topic goes quiet for an abnormal stretch, page someone

Because your handlers are already idempotent, reconciliation reuses the exact same upsert path as live webhooks — re-processing overlap is harmless. Hookdeck's Shopify webhooks best-practices guide covers monitoring and alerting patterns for exactly these gaps.

Best Practices vs. Costly Mistakes

Best practiceCostly mistake
Verify HMAC with raw body + timing-safe compareSkipping verification "temporarily" in production
Return 200 fast, process via queueDoing sync/email/API work inline before responding
Dedupe on X-Shopify-Webhook-Id + upsert by resource IDAssuming each event arrives exactly once, in order
Declarative TOML subscriptions in version controlHand-created subscriptions nobody remembers exist
Hourly reconciliation + subscription auditsTrusting webhooks as the sole source of truth
Implementing all three compliance topics before reviewDiscovering the requirement from a rejection email

Testing Shopify Webhooks Locally

Shopify webhooks need a publicly reachable URL, and localhost:3000 isn't one. The current tooling makes this nearly frictionless.

shopify app dev Handles the Tunnel

Run shopify app dev and the CLI starts your app and provisions a temporary public tunnel (Cloudflare-backed) that forwards to your local server. Your TOML-declared subscriptions are pointed at the tunnel for the dev session, so real events from your development store — place a test order, edit a product — flow straight into your local handler with genuine payloads and valid signatures.

Triggering Sample Events on Demand

For fast iteration without clicking through the admin, the CLI ships a dedicated command:

bashbash
shopify app webhook trigger \
  --topic orders/create \
  --api-version 2026-04 \
  --address http://localhost:3000/webhooks \
  --client-secret $SHOPIFY_CLIENT_SECRET

It sends a sample payload for the topic to any address — localhost HTTP, a remote HTTPS URL, a pubsub:// URI, or an EventBridge ARN. Pass --client-secret so the delivery is signed with a key you control and your HMAC verification runs for real instead of being stubbed out. Omit any flag and the CLI prompts interactively.

What to Test Before You Ship

Run this checklist against your handler — locally, then again on staging:

  • Valid signature → 200; tampered body → 401
  • The same delivery sent twice → exactly one side effect
  • orders/updated arriving before orders/create → correct final state
  • Handler responds in well under 5 seconds with a queue backlog
  • All three compliance topics → 200 (and 401 on bad HMAC)

If you're stuck on a verification mismatch or a subscription that won't fire, the Talk Shop dev community on Discord is full of app developers who have debugged the exact same raw-body bug — it's usually a faster answer than a forum thread.

Ship Webhooks That Survive the Real World

A close-up photograph of a developer's keyboard and mouse in a dark room with intense electric blue lighting.

Shopify webhooks are easy to start and unforgiving to run carelessly. The recipe that holds up in production:

  • Subscribe declaratively in shopify.app.toml; reach for webhookSubscriptionCreate only when configuration varies per shop
  • Verify every delivery — HMAC over the raw body, timing-safe comparison, 401 on mismatch
  • Respect the 5-second budget — acknowledge fast, process in a queue
  • Design for at-least-once — dedupe on X-Shopify-Webhook-Id, upsert by resource ID, never trust ordering
  • Reconcile on a schedule — webhooks are a freshness optimization, the Admin API is the source of truth
  • Ship the compliance trio before app review asks

Get those six right and webhooks become the most boring part of your stack — which is exactly what event infrastructure should be. For more deep dives like this one, browse the Shopify development category on our blog, and bring your webhook war stories to the Talk Shop Discord dev community — we'd genuinely like to hear them.

What's the nastiest webhook bug you've shipped — a duplicate-processing incident, a silent subscription removal, or something stranger?

Shopify DevelopmentAutomation
Talk Shop

About Talk Shop

The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.

Related Insights

Related

Headless CMS for Ecommerce: Choosing the Right One for Shopify (2026)

Related

Shopify Partner Program: How It Works and Is It Worth It (2026)

New

Business Name Generator

Generate unique, brandable business names with AI. Check domain availability instantly.

Generate Names

Talk Shop Daily

Daily ecommerce news, teardowns, and tactics.

No spam. Unsubscribe anytime. · Learn more

Try our Business Name Generator

Join the Best Ecommerce Newsletter
for DTC Brands

12-18 curated ecommerce stories from 100+ sources, delivered every morning in under 5 minutes. Trusted by 10,000+ operators.

No spam. Unsubscribe anytime. · Learn more

Join the Community

300+ Active

Connect with ecommerce founders, share wins, get feedback on your store, and access exclusive discussions.

Join Discord Server