How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Shopify webhook?
A Shopify webhook is an HTTP POST sent to an address subscribed to a topic such as orders/create, products/update or app/uninstalled. The body is the resource itself — an order, a product, a customer — in the shape of the API version named in X-Shopify-API-Version. There is no envelope: the topic and the shop it came from are only in the headers.
Subscriptions belong to an app. They can be declared in the app's configuration file, created through the Admin API, or — for a single store — added by a merchant under Settings → Notifications → Webhooks.
What a Shopify webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Shopify-Topic: orders/create
X-Shopify-Hmac-Sha256: XWmrwMey6OsLMeiZKwP4FppHH3cmAiiJJAweH5Jo4bM=
X-Shopify-Shop-Domain: example-store.myshopify.com
X-Shopify-API-Version: 2025-10
X-Shopify-Webhook-Id: b54557e4-bdd9-4b37-8a5f-bf7d70bcd043
X-Shopify-Event-Id: 98880550-7158-44d4-b7cd-2c97c8a091b5
X-Shopify-Triggered-At: 2026-01-01T12:00:00.123456789Z
{
"id": 820982911946154500,
"admin_graphql_api_id": "gid://shopify/Order/820982911946154508",
"name": "#1001",
"order_number": 1001,
"created_at": "2026-01-01T07:00:00-05:00",
"currency": "USD",
"total_price": "59.90",
"subtotal_price": "49.90",
"total_tax": "0.00",
"financial_status": "paid",
"fulfillment_status": null,
"test": true,
"email": "jon@example.com",
"customer": {
"id": 115310627314723950,
"email": "jon@example.com",
"first_name": "Jon",
"last_name": "Doe"
},
"line_items": [
{
"id": 866550311766439000,
"title": "Short Sleeve T-Shirt",
"quantity": 1,
"price": "49.90",
"sku": "TS-BLK-M",
"variant_id": 808950810
}
],
"shipping_lines": [
{
"title": "Standard",
"price": "10.00"
}
]
}
Prices are decimal strings. Ids are 64-bit integers that exceed what a JavaScript number can hold exactly — parse them as strings or use admin_graphql_api_id. test: true marks an order placed through a test gateway.
Headers Shopify sends
| Header | Example | What it is |
|---|---|---|
X-Shopify-Topic | orders/create | The event. The body does not repeat it. |
X-Shopify-Hmac-Sha256 | XWmrwMey6OsLMeiZKwP4FppHH3cmAiiJJAweH5Jo4bM= | HMAC-SHA256 of the raw body, base64-encoded. |
X-Shopify-Shop-Domain | example-store.myshopify.com | Which store sent it — the key for looking up the right tenant. |
X-Shopify-API-Version | 2025-10 | API version the payload was serialised with. |
X-Shopify-Webhook-Id | b54557e4-bdd9-4b37-8a5f-bf7d70bcd043 | Unique id of this delivery, constant across retries. |
X-Shopify-Event-Id | 98880550-7158-44d4-b7cd-2c97c8a091b5 | Id of the underlying event. Several subscriptions can receive the same event. |
X-Shopify-Triggered-At | 2026-01-01T12:00:00.123456789Z | When the event happened, which is not when it was delivered. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Shopify signs a webhook
Shopify computes an HMAC-SHA256 of the raw request body using the app's client secret (called the API secret key in older screens) and sends the digest base64-encoded in X-Shopify-Hmac-Sha256. Webhooks created by a merchant in the store admin are signed with a key shown on that notifications page instead.
Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function. Compare against the base64 form: hex-encoding your digest is the single most common reason this check fails.
const crypto = require('crypto');
// rawBody: Buffer of the body exactly as received
function verifyShopify(rawBody, header, clientSecret) {
const expected = crypto.createHmac('sha256', clientSecret)
.update(rawBody).digest('base64');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The five-second rule and retries
Shopify waits about five seconds for a response, per its documentation at the time of writing, and treats anything other than a 2xx in that window as a failure. That is short enough that doing real work inline — calling another API, writing several rows — will eventually time out under load. Store the payload, return 200, process afterwards.
Failed deliveries are retried for a limited period measured in hours, with growing gaps. A subscription created through the API that keeps failing through that period is removed, and the app has to re-register it — so a long outage does not just lose events, it can silently end the subscription. Reconcile with a periodic API fetch for anything that matters.
Catch a real Shopify webhook
- Press Create webhook URL at the top of this page and copy the address.
- In a development store's admin, open Settings → Notifications → Webhooks and choose Create webhook.
- Pick an event such as Order creation, keep the format as JSON, paste the address and save.
- Use Send test notification on that row, or place a test order.
- Read the headers and the order JSON in the inbox above.
App developers can do the same from a terminal with the Shopify CLI's shopify app webhook trigger command, pointing --address at the URL.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Base64, not hex. Worth repeating, because the failure looks exactly like a wrong secret.
- Duplicates are normal. De-duplicate on
X-Shopify-Webhook-Id(or the event id), and expectorders/updatedto fire many times for one checkout. - Mandatory privacy webhooks. Public apps must handle
customers/data_request,customers/redactandshop/redact, and must reject an invalid HMAC with401.
api-tester.com is independent and is not affiliated with Shopify. Providers change header names, limits and retry schedules; treat the official Shopify documentation as the source of truth.
Related examples and tools
Zapier Webhook Example
Zapier works in both directions.
Slack Webhook Example
The JSON Slack's Events API posts to your Request URL, the one-time url_verification handshake, and the v0 signature that covers a timestamp as well as the body.
Discord Webhook Example
A Discord webhook runs the other way from most: you send the request, and Discord posts it in a channel.
Twilio Webhook Example
Twilio's webhooks are form posts, not JSON, and the signature covers your URL as well as the parameters.
GitLab Webhook Example
The JSON a GitLab project webhook posts on a push, the headers that describe it, and the secret token that is compared rather than computed.
Base64 Encoder
Convert a digest between hex and base64 when a signature comparison will not line up.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official Shopify documentation — The provider's own guide to the requests described on this page.
- Shopify.dev: verify webhook deliveries — The HMAC check for HTTPS deliveries.
- Shopify.dev: troubleshoot webhooks — What to check when deliveries fail or go missing.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Shopify webhook FAQ
What does a Shopify webhook payload look like?
The JSON of the resource itself — an order for orders/create, a product for products/update — with no wrapper. The topic, shop domain and API version are sent as headers.
How do I verify a Shopify webhook?
Compute an HMAC-SHA256 of the raw body with the app's client secret, base64-encode the digest and compare it to the X-Shopify-Hmac-Sha256 header using a constant-time comparison.
How fast must I respond to a Shopify webhook?
Within roughly five seconds according to Shopify's documentation at the time of writing. Acknowledge immediately and process the payload asynchronously.
What happens if my Shopify webhook endpoint is down?
Shopify retries for a limited period. If an API-created subscription keeps failing, Shopify removes it and the app must subscribe again, so reconcile important data with the API.
Which header tells me which store sent the webhook?
X-Shopify-Shop-Domain carries the store's myshopify.com domain, and X-Shopify-Topic names the event.