Short answer
Verify the HMAC signature on the raw body, store the event durably keyed by X-Shopify-Webhook-Id, and return a 2xx response well within Shopify's five-second limit. Do all real processing asynchronously, fetch current data from the API rather than trusting payload order, alert when deliveries fail, and run reconciliation because Shopify removes subscriptions after persistent failures.
- Shopify delivery POST with HMAC header
- Verify HMAC on raw body
- Store Unique by webhook ID
- Respond 200 Well under 5 seconds
- Async worker Fetch + process
The receiver does three things and nothing else. Everything slow happens after the 200 response.
Webhooks are the backbone of an event-driven Shopify integration and its most common point of silent failure. A receiver that is slow, strict about ordering, or careless with duplicates will work in testing and quietly lose or corrupt data in production. This guide shows how to build one that holds up.
The documented contract
Shopify’s documentation defines how webhook delivery behaves. Design directly against these points:
A 2xx response is a success. If your app does not respond within five seconds, the delivery fails. Failed deliveries are retried up to eight times over four hours, with increasing intervals; if failures persist, the subscription is removed. Ordering is not guaranteed within or across topics, and apps should ignore duplicate deliveries using X-Shopify-Webhook-Id. Shopify advises not relying solely on webhooks and running reconciliation jobs.
Each delivery carries metadata headers including X-Shopify-Topic, X-Shopify-Webhook-Id, X-Shopify-Hmac-Sha256, X-Shopify-Shop-Domain, and X-Shopify-Triggered-At.
Step 1: Get the raw body
HMAC verification must run over the exact bytes Shopify sent. If your framework parses JSON first and you re-serialize it, whitespace and key order can change and the signature will not match.
In Express, mount a raw body parser only on the webhook route:
import express from 'express';
const app = express();
// Raw bytes for webhooks only; JSON parsing elsewhere.
app.post('/webhooks/shopify', express.raw({ type: 'application/json', limit: '2mb' }), handleWebhook);
app.use(express.json());
In other runtimes, read the request body as an ArrayBuffer or bytes before any parsing.
Step 2: Verify the signature
Compute an HMAC-SHA256 of the raw body using your app’s client secret, base64-encode it, and compare it to the header with a constant-time comparison:
import crypto from 'node:crypto';
function verifyShopifyHmac(raw: Buffer, header: string | undefined, secret: string): boolean {
if (!header) return false;
const expected = crypto.createHmac('sha256', secret).update(raw).digest('base64');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(header, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Return 401 for invalid signatures and log the attempt (without the body). During secret rotation, accept either the old or new secret for a short, defined window.
Step 3: Store, then acknowledge
Write the event to durable storage and respond immediately. Use the webhook ID as a unique key so retries and duplicates collapse into one row:
async function handleWebhook(req: express.Request, res: express.Response) {
if (!verifyShopifyHmac(req.body, req.get('X-Shopify-Hmac-Sha256'), process.env.SHOPIFY_CLIENT_SECRET!)) {
return res.sendStatus(401);
}
try {
await db.query(
`INSERT INTO inbound_events (webhook_id, shop, topic, triggered_at, payload)
VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (webhook_id) DO NOTHING`,
[
req.get('X-Shopify-Webhook-Id'),
req.get('X-Shopify-Shop-Domain'),
req.get('X-Shopify-Topic'),
req.get('X-Shopify-Triggered-At'),
req.body.toString('utf8'),
],
);
return res.sendStatus(200);
} catch (err) {
// Storage failed: let Shopify retry rather than acknowledging an event we did not keep.
logger.error({ err }, 'failed to persist webhook');
return res.sendStatus(500);
}
}
The one situation where you should not return 200 is when you failed to store the event. A 5xx tells Shopify to retry; a 200 for an unstored event loses it permanently.
Step 4: Process asynchronously
A worker reads stored events and processes them. Three rules keep processing correct regardless of delivery order:
- Treat the webhook as a notification. Extract the resource ID and fetch the current resource from the GraphQL Admin API. If two updates arrive out of order, both fetches return the latest state.
- Skip stale work. Store the
updatedAtof the last version you wrote for each resource. If a fetched resource is not newer, mark the event done without writing. - Serialize per resource. Ensure two workers do not process the same order simultaneously. See the architecture guide for locking options.
Step 5: Watch for failures and removed subscriptions
Because Shopify removes subscriptions after persistent failures, a receiver outage can silently end your integration. Protect against it:
- Alert on delivery failure rates. Shopify’s developer dashboard shows webhook delivery metrics; Shopify’s troubleshooting guide flags failure rates above 0.5% as higher than average.
- Alert on response time. Shopify lists response times between four and five seconds as a symptom to fix.
- Verify subscriptions on a schedule. A daily job that lists your webhook subscriptions through the API and recreates missing ones is cheap insurance.
- Alert on silence. If a store that normally receives orders every few minutes has not sent
orders/createfor an hour during business hours, something is wrong.
Step 6: Reconcile
Even a perfect receiver can miss events — during a deployment, a DNS problem, or a regional outage. Run a scheduled query for resources updated since the last checkpoint and enqueue anything not already processed. The webhooks vs. polling guide shows how to combine both approaches.
Alternative delivery targets
Instead of HTTPS, Shopify can deliver webhooks to Amazon EventBridge or Google Cloud Pub/Sub. This removes the need to operate a public receiver and the five-second response concern, since the cloud service accepts the event. You still need consumers that deduplicate, fetch current state, and handle failures.
Security considerations
- Reject unsigned or invalid requests before any parsing or database work.
- Use HTTPS only, and keep the endpoint path unguessable as defense in depth (not as a replacement for HMAC).
- Rate-limit the endpoint at the edge to absorb abuse without affecting legitimate deliveries.
- Limit request body size.
- Treat payloads as untrusted input: validate types before using values in queries.
- Restrict access to stored payloads, which may contain customer personal data.
Testing
- Unit test HMAC verification with a stored real payload and its header, plus a tampered body.
- Send the same payload twice and confirm one stored row.
- Deliver an older update after a newer one and confirm the newer state wins.
- Make the database unavailable and confirm the receiver returns 5xx.
- Measure p99 response time under a burst of deliveries; it should stay far below five seconds.
- Use Shopify CLI’s webhook trigger command or a development store to send test events end to end.
Common mistakes
- Parsing JSON before verifying the HMAC.
- Doing ERP calls or Shopify API calls inside the request.
- Returning 200 even when storing the event failed.
- Using the payload order to decide which version is newer.
- No monitoring of subscription health until orders stop arriving.
Summary
A reliable webhook receiver verifies the HMAC on the raw body, stores the event under its webhook ID, and responds quickly — and nothing else. Processing happens later, fetches current data, skips stale versions, and runs one resource at a time. Monitor delivery failures and subscription health, and reconcile on a schedule, because Shopify documents that webhooks alone are not a guarantee.
Frequently asked questions
How long does Shopify wait for a webhook response?
Shopify's troubleshooting documentation states that if your app doesn't respond within five seconds, the delivery fails. Aim to respond in well under a second by deferring work to a queue.
How many times does Shopify retry a failed webhook?
Shopify documents up to eight retries within a four-hour period. If failures persist, the subscription is removed and must be recreated.
Why does my HMAC verification fail even with the correct secret?
The most common cause is computing the HMAC on a parsed and re-serialized JSON body instead of the exact raw bytes. Frameworks that parse JSON automatically must be configured to give you the raw body for the webhook route.
Are Shopify webhooks delivered in order?
No. Shopify states it does not guarantee ordering within a topic or across topics for the same resource. Use the X-Shopify-Triggered-At header or the resource's updated_at timestamp to decide which version is newest.
Sources and further reading
Platform-specific statements on this page were checked against these sources on . Vendor APIs change; confirm details in the current documentation before you build.
- [1] Webhook best practices — Shopify
- [2] Troubleshooting webhooks — Shopify
- [3] Deliver webhooks through HTTPS — Shopify
- [4] RFC 2104: HMAC: Keyed-Hashing for Message Authentication — IETF
Related reading
- Guides Shopify Webhooks vs. Polling for ERP Integrations When to use Shopify webhooks, when to poll the GraphQL Admin API, and how to implement the hybrid pattern Shopify itself recommends for ERP integrations.
- Concepts What Are Webhooks? What webhooks are, how they deliver change notifications between systems, how to verify them, and the delivery behaviors integrations must be designed around.
- Guides How to Prevent Duplicate Orders in an ERP Integration Why ecommerce integrations create duplicate ERP orders — duplicate webhooks, retries after timeouts, races — and the layered defenses that stop them.
- Guides How to Handle Failed ERP Synchronization Jobs What to do when integration jobs fail — triage categories, dead-letter queues, review screens, safe replay, ownership, and the alerts that prevent silent data loss.
- Guides How to Design an Integration Monitoring System What to measure in an ecommerce-ERP integration, how to set alert thresholds, which dashboards help, and how business-level checks catch silent failures.
- Platforms Shopify ERP Integration How Shopify connects to an ERP — the GraphQL Admin API, webhooks, inventory states, fulfillment orders, rate limits — plus the sync patterns and pitfalls that matter.