Skip to content
TryWebhook
New URL

Commerce · Free · No signup

Test and debug Shopify webhooks

Register a throwaway URL as a Shopify webhook, create a test order, and read the delivery as it arrived — including the base64 HMAC and the five X-Shopify headers that carry everything the body does not.

No signup · No email · Ready in about a second

At a glance

Shopify webhooks in brief

Category
Commerce
Where to configure
Store settings → Notifications → Webhooks (or your app's TOML config for app webhooks)
Signature header
X-Shopify-Hmac-Sha256
Secret
API secret key (client secret)
Retries
19 attempts over 48 hours with exponential backoff. If every attempt fails, Shopify deletes the subscription and emails the app owner.
Response deadline
5 seconds. The tightest deadline of any major commerce platform.
Treated as success
Any 2xx. Anything else is a failed attempt and counts towards the 19.

Cross-checked against Shopify's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Shopify actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}497 B

Shopify puts the topic and shop domain in headers.

5 headers
x-shopify-topic
orders/create
x-shopify-shop-domain
example-store.myshopify.com
x-shopify-api-version
2025-07
x-shopify-webhook-id
b1a2c3d4-e5f6-7890-abcd-ef1234567890
x-shopify-triggered-at
2026-06-15T10:04:00.000Z
Body · application/json · 1 line
{"id":5555555555555,"admin_graphql_api_id":"gid://shopify/Order/5555555555555","name":"#1001","order_number":1001,"currency":"USD","total_price":"49.90","subtotal_price":"44.90","total_tax":"5.00","financial_status":"paid","fulfillment_status":null,"created_at":"2026-06-15T10:04:00-04:00","customer":{"id":6666666666,"email":"buyer@example.com","first_name":"Ada"},"line_items":[{"id":7777777777,"title":"Mechanical Keyboard","quantity":1,"price":"44.90","sku":"KB-001","variant_id":8888888888}]}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Shopify signature

Signed payload

A base64-encoded HMAC-SHA256 of the raw request body, with no prefix and no timestamp. Keyed with the app's client secret for app webhooks, or with the store's own webhook secret for ones created in the admin.

Implemented as: HMAC-SHA256 over the raw body, base64.

Built in. Open any captured request, switch to the Signature tab, pick Shopify, and paste your api secret key (client secret). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
orders/createAn order was placed, paid or not. Read `financial_status` before you fulfil anything — a draft or pending order arrives here too.
orders/paidPayment was captured. This, not `orders/create`, is the fulfilment trigger for most stores.
products/updateAny field on a product or one of its variants changed. Fires often, including for inventory movements, so debounce before you sync.
app/uninstalledThe merchant removed your app. Your last chance to stop billing and delete their data — the access token is dead immediately after.
customers/data_requestA GDPR data request. Mandatory for public apps, and Shopify's app review actively tests that you verify its HMAC.

Registering a test URL

The quickest route needs no app at all. In your store, open Settings → Notifications, scroll to Webhooks, and press Create webhook. Choose an event, paste the URL from above, pick JSON, and save. Shopify sends nothing on creation, so place a test order — the Bogus Gateway lets you do that on a development store without money moving.

Note the signing secret printed under the webhook list. Admin-created webhooks use that, not your app’s client secret. This is the single most common reason a verification function that works in production fails while you are testing by hand.

For an app, register the subscription in your shopify.app.toml or through the GraphQL Admin API:

mutation {
  webhookSubscriptionCreate(
    topic: ORDERS_CREATE
    webhookSubscription: { callbackUrl: "https://trywebhook.com/w/your-bin-id", format: JSON }
  ) { userErrors { field message } }
}

The headers carry the routing

Look at the sample above and notice how much lives outside the body:

X-Shopify-Topic: orders/create
X-Shopify-Shop-Domain: example-store.myshopify.com
X-Shopify-API-Version: 2025-07
X-Shopify-Webhook-Id: b1a2c3d4-e5f6-7890-abcd-ef1234567890
X-Shopify-Triggered-At: 2026-06-15T10:04:00.000Z

An orders/create body and an orders/updated body are the same JSON document. If you route on anything other than X-Shopify-Topic, you will treat an edit as a new order. And because an app receives webhooks from every store that installed it through one URL, X-Shopify-Shop-Domain is what tells you whose order this is.

X-Shopify-API-Version is worth logging. Shopify pins each subscription to the version current when it was created, and fields appear and disappear between versions. When a field the docs promise is missing, that header explains it.

Verifying the HMAC

X-Shopify-Hmac-Sha256: aGVsbG8gdGhlcmUsIHRoaXMgaXMgbm90IHJlYWw=

Base64, over the raw body, no timestamp and no prefix. In the Signature tab above, choose Shopify and paste the secret; it computes the digest over the exact bytes that arrived.

Because there is no timestamp inside the signature, a captured Shopify delivery remains valid indefinitely. Deduplicating on X-Shopify-Webhook-Id closes that, and you need it for retries anyway.

One structural point: verify before you parse. Shopify’s raw body uses no whitespace, but JSON.parse followed by JSON.stringify will reorder nothing and still change enough — number formatting, Unicode escapes — to break the digest on some payloads. Keep the bytes.

Money is a string

"total_price": "49.90",
"subtotal_price": "44.90",
"total_tax": "5.00"

Shopify sends decimal strings, not integers of minor units the way Stripe and Paddle do. Parsing them into a float and adding them up is how stores end up a cent out on a reconciliation report. Parse to a decimal type, or multiply to integer cents before you do arithmetic.

Also note id: 5555555555555 — a 64-bit integer. In JavaScript that is beyond Number.MAX_SAFE_INTEGER territory for some Shopify ids, and the admin_graphql_api_id string is the safer key.

Debugging the five-second timeout

If Shopify’s delivery log shows timeouts, the handler is doing work inline. The fix is structural rather than a matter of optimisation:

  1. Verify the HMAC.
  2. Write the raw body, the topic, and the webhook id to a queue or table.
  3. Return 200.
  4. Process from the queue, where nothing is waiting on you.

Copy a captured request as cURL and time your own endpoint against it. If it takes more than about two seconds locally, it will time out on a store under load.

Failure modes

Where Shopify integrations usually break

Five seconds, total

Shopify waits five seconds for a 2xx and no longer. An `orders/create` handler that calls a tax API or writes to a third-party ERP inline will time out on a busy store, and the retry will do it again. Acknowledge first, then work.

The topic is in a header

Nothing in the body says whether you are looking at `orders/create` or `orders/updated` — the shapes are identical. `X-Shopify-Topic` is the only discriminator, and `X-Shopify-Shop-Domain` is the only thing identifying which store sent it. Multi-tenant apps that ignore the second one write orders into the wrong tenant.

Two different secrets

Webhooks created by an app are signed with the app's client secret. Webhooks a merchant creates by hand in Settings → Notifications are signed with a separate secret shown on that page. Same header, same algorithm, different key — and a verifier hard-coded to one will reject the other.

Base64, not hex

Shopify is the outlier here: the digest is base64-encoded. A verifier ported from Stripe or GitHub will hex-encode and fail every time, with no clue in the error message as to why.

Duplicate deliveries are normal

`X-Shopify-Webhook-Id` is stable across retries of the same event. Store it. Without it, a slow handler that eventually returns 200 will have already been retried, and the order gets fulfilled twice.

Answers

Shopify webhook questions

Can I test Shopify webhooks without a paid store?+

Yes. A development store from a Shopify Partner account is free and sends real webhooks, including real HMAC headers. You can create test orders in it with the Bogus Gateway.

Why does my HMAC verification always fail?+

Two causes cover almost all of it: the digest is base64 and not hex, and the body must be the raw bytes rather than a re-serialised JSON object. Verify the captured request here with the same secret — if it passes here, the problem is one of those two.

Which secret do I use?+

For webhooks your app registers, the app's client secret from the Partner dashboard. For webhooks a merchant added under Settings → Notifications, the signing secret printed at the bottom of that same page. They are not interchangeable.

What happens if my endpoint is down for a day?+

Shopify keeps retrying for 48 hours, so a short outage costs you nothing. After 48 hours of total failure it removes the subscription, and events during that gap are gone — you have to reconcile through the Admin API.

Do I have to handle the GDPR webhooks?+

For a public app in the Shopify App Store, yes — `customers/data_request`, `customers/redact`, and `shop/redact` are mandatory and reviewed. They arrive with the same HMAC header as everything else, and a handler that returns 401 will fail review.

Other providers

Testing something else?

One click

Point Shopify at a URL and watch the payload land.

Store settings → Notifications → Webhooks (or your app's TOML config for app webhooks) — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second