Webhook Studio
/Shopify webhooks
Webhooks / Shopify

Shopify webhooks

Capture Shopify webhooks and see why the 5-second timeout keeps failing them.

Get a test URL

Get a live Shopify webhook URL
No signup, no install. The URL works the moment you click, and every request Shopify sends is stored with its full headers and raw body.
Free, anonymous, and expires after 3 days.
Building this with an AI agent? Webhook Studio is also an MCP server, so your agent can provision this endpoint, wait for the Shopify event, replay it into localhost, and generate types from the real payload — keyless, no human in the browser. One-line install:
claude mcp add --transport http webhook-studio https://webhook-studio.com/mcp \
  --header "Authorization: Bearer $(curl -s -X POST https://webhook-studio.com/api/start | jq -r .api_token)"

Point Shopify at this URL

  1. Create a test endpoint below and copy its URL.
  2. For an app: set the URL in your Partners dashboard under your app → Configuration → Webhooks (or subscribe via the Admin API).
  3. For a single store: Shopify admin → Settings → Notifications → Webhooks → Create webhook.
  4. Choose the topic (for example orders/create) and paste the Webhook Studio URL.
  5. Trigger it — place a test order, or use the "Send test notification" button — and watch it land in the Inbox.
Nothing to install. Webhook Studio sits between Shopify and your app. Every delivery is stored and inspectable, and routing rules forward the ones you care about to your real backend — with retries, and without losing the request history when your backend is down.

What a Shopify delivery looks like

A representative body, using Shopify’s field names. Your own deliveries appear in the Inbox exactly as sent, headers and raw bytes included.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
  "id": 5123456789012,
  "admin_graphql_api_id": "gid://shopify/Order/5123456789012",
  "name": "#1001",
  "order_number": 1001,
  "created_at": "2026-07-21T09:14:02+08:00",
  "currency": "USD",
  "total_price": "48.00",
  "subtotal_price": "44.00",
  "total_tax": "4.00",
  "financial_status": "paid",
  "fulfillment_status": null,
  "test": true,
  "customer": {
    "id": 6234567890123,
    "email": "customer@example.com",
    "first_name": "Alex",
    "last_name": "Rivera"
  },
  "line_items": [
    {
      "id": 14234567890123,
      "title": "Field Notes — 3 pack",
      "quantity": 2,
      "price": "22.00",
      "sku": "FN-3PK",
      "product_id": 8234567890123,
      "variant_id": 44234567890123
    }
  ],
  "shipping_address": {
    "city": "Portland",
    "province": "Oregon",
    "country": "United States",
    "zip": "97205"
  }
}

Delivery headers

HeaderWhat it carries
X-Shopify-Hmac-SHA256Base64-encoded HMAC-SHA256 of the raw body, keyed on your app's client secret. Note there is no "sha256=" prefix — unlike GitHub, the header is the bare digest.
X-Shopify-TopicThe event name, e.g. orders/create. Shopify puts the topic in a header, not the body.
X-Shopify-Shop-DomainThe shop this delivery belongs to, e.g. example.myshopify.com. Essential once one app serves many shops.
X-Shopify-Webhook-IdStable id for this delivery. The correct idempotency key — it is unchanged across retries of the same event.
X-Shopify-Event-IdCorrelates separate deliveries produced by one merchant action.
X-Shopify-API-VersionThe API version the payload was serialised with, e.g. 2025-07.
X-Shopify-Triggered-AtWhen the event was triggered in Shopify, distinct from when delivery was attempted.

Event types

The events worth handling first. See Shopify’s own documentation for the complete list.

EventFires when
orders/createAn order is placed. The one most integrations start with.
orders/paidAn order is marked paid. Fulfil on this rather than orders/create if payment can be pending.
orders/cancelledAn order is cancelled, whether by staff or automatically.
orders/fulfilledEvery line item on an order has been fulfilled.
products/createA product is added to the catalogue.
products/updateA product changes. Fires often — inventory edits count — so filter before acting.
customers/createA customer record is created.
app/uninstalledYour app is removed from a shop. Handle this one: it is how you learn to stop billing and purge that shop's data.
checkouts/createA checkout begins. Used for abandoned-cart flows.

Verify the X-Shopify-Hmac-SHA256 header

Shopify signs each delivery with HMAC-SHA256 over the raw request body, keyed on your webhook secret. Verify before you parse, and before you act.

Node.js (Express)
import express from 'express';
import crypto from 'crypto';

const app = express();

// express.raw() matters: the HMAC covers the exact bytes Shopify sent. A
// global express.json() upstream would reparse them and every check fails.
app.post('/webhooks/shopify', express.raw({ type: 'application/json' }), (req, res) => {
  const sent = req.get('X-Shopify-Hmac-SHA256') ?? '';
  const digest = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET) // app client secret
    .update(req.body)                                     // Buffer, not a string
    .digest('base64');                                    // base64, NOT hex

  const a = Buffer.from(digest, 'base64');
  const b = Buffer.from(sent, 'base64');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  // Acknowledge inside 5 seconds or Shopify calls it a failure and retries.
  // Queue the work; never do it before responding.
  res.sendStatus(200);

  const topic = req.get('X-Shopify-Topic');       // 'orders/create'
  const deliveryId = req.get('X-Shopify-Webhook-Id'); // dedupe on this
  // enqueue({ topic, deliveryId, body: JSON.parse(req.body) })
});

Retries and delivery guarantees

Shopify allows a one-second connection timeout and five seconds for the whole request. Anything outside the 200 range — including 3xx redirects — is a failure. Failed deliveries are retried 8 times over roughly 4 hours, and if every attempt fails, a subscription created through the Admin API is deleted automatically and warning emails go to the store's emergency contacts. Losing the subscription entirely, not just the event, is what makes Shopify's timeout stricter in practice than most.

Common pitfalls

Expecting a "sha256=" prefix

GitHub sends sha256=<hex>; Shopify sends bare base64 with no prefix at all. Verifiers written against the GitHub shape, or ported from it, compare a base64 string to a hex one and fail every delivery. Compare decoded bytes, not strings.

Doing the work before responding

The budget is five seconds for the entire request. Any real fulfilment logic — an API call, a slow query — will blow through it under load. Shopify then retries, and after enough failures deletes the subscription, so a slow handler silently turns into no webhooks at all.

Deduplicating on the wrong field

Retries reuse X-Shopify-Webhook-Id, so that is the idempotency key. X-Shopify-Event-Id groups the several deliveries one merchant action produces, which is a different thing — keying on it collapses events you wanted to keep.

Trusting order for inventory

products/update fires on every inventory change and Shopify makes no ordering guarantee, so two rapid edits can arrive reversed. Read current state from the Admin API when the value matters rather than reconstructing it from the stream.

FAQ

How do I test Shopify webhooks without deploying?

Create a test endpoint on this page and paste its URL into your app configuration or the store's Notifications settings. The full request — every X-Shopify header, the raw body and the HMAC — appears in the Inbox immediately, with no code deployed and no account required.

Why does my Shopify HMAC verification always fail?

Two causes account for almost all of it. The header is bare base64 with no "sha256=" prefix, so code ported from GitHub compares mismatched encodings. And the digest covers the raw bytes, so any JSON body-parser mounted ahead of the route invalidates every signature.

What happens if my endpoint is too slow for Shopify?

Shopify allows five seconds for the whole request. Slower is a failure, retried 8 times over about 4 hours; if all attempts fail, an Admin API subscription is deleted outright. Return 200 first and queue the work — and while you are debugging, point Shopify here so the payloads are captured even when your own server times out.

Which header should I use to deduplicate Shopify webhooks?

X-Shopify-Webhook-Id. It stays the same across retries of a delivery, which is exactly what an idempotency key needs. X-Shopify-Event-Id is for correlating the different deliveries a single merchant action produced.

Can I forward Shopify webhooks to localhost?

Yes. Add a routing rule on your endpoint pointing at your local tunnel and matching events are forwarded with retries while still being stored here — so you keep the full history even for the deliveries your local server was too slow to answer.