Shopify Webhooks: Setup, Verification, and Common Pitfalls
How to receive, verify, and reliably process Shopify webhook events. Covers topic subscriptions, HMAC verification, compliance webhooks, and the mistakes that break integrations.
Shopify webhooks look similar to Stripe or GitHub's at first glance: HMAC-signed HTTP POST requests. The details diverge quickly once you start building against them. The signature encoding is different, subscriptions work per-topic rather than per-webhook, and public apps carry mandatory compliance requirements that have nothing to do with the app's actual functionality.
This guide covers what's specific to Shopify.
Topics, not one webhook for everything
GitHub lets you pick from a checkbox list of events on a single webhook. Shopify instead treats each event type, called a topic, as its own subscription.
orders/create
orders/updated
orders/cancelled
products/create
products/update
customers/create
app/uninstalled
You can point every topic at the same URL. Route based on the X-Shopify-Topic header once the request lands, rather than registering a different endpoint per topic.
Create subscriptions via the Admin API or CLI
For a custom app, use the Admin API. For a public app built with the Shopify CLI, topics are usually declared in your app's configuration and registered automatically on install.
shopify app webhooks trigger --topic=orders/create
Point every subscription at one endpoint
You don't need a route per topic. One handler, dispatched by header, is simpler to maintain.
Verify and acknowledge fast
Shopify gives you 5 seconds to respond. Verify the signature, queue the payload, return 200.
Verifying the signature
Shopify signs the raw request body with your app's client secret, using HMAC-SHA256, and base64-encodes the result into the X-Shopify-Hmac-SHA256 header. If you're coming from GitHub or Stripe, the algorithm is the same but the encoding isn't: those use hex, Shopify uses base64. Reusing verification code without changing the encoding is a common first bug.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyShopifyWebhook(
rawBody: Buffer,
hmacHeader: string | undefined,
secret: string
): boolean {
if (!hmacHeader) return false;
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('base64');
try {
return timingSafeEqual(
Buffer.from(hmacHeader),
Buffer.from(expected)
);
} catch {
return false;
}
}
Shopify signs the exact bytes received, so anything that reformats the JSON body before verification (a middleware that re-serializes it, a proxy that normalizes whitespace) breaks the check. Verify against the raw buffer, before any body parser touches it.
The 5-second window
Shopify expects a response within 5 seconds, tighter than GitHub's 10 or Stripe's 20. If your endpoint doesn't respond in time, or returns an error, Shopify retries 8 times over the next 4 hours, with most attempts front-loaded into the first half hour.
app.post('/webhooks/shopify', async (req, res) => {
const topic = req.headers['x-shopify-topic'] as string;
const hmac = req.headers['x-shopify-hmac-sha256'] as string;
if (!verifyShopifyWebhook(req.rawBody, hmac, process.env.SHOPIFY_CLIENT_SECRET!)) {
return res.status(401).send('Invalid signature');
}
// Acknowledge immediately
res.status(200).send('ok');
// Process asynchronously, well outside the 5-second window
void processShopifyWebhook(topic, req.body);
});
Anything beyond a fast, guaranteed-quick database write belongs in the async path. A slow handler and a broken one produce identical retry behaviour from Shopify's side.
Deduplication: use the delivery ID, not the payload
Shopify includes an X-Shopify-Webhook-Id header, a UUID unique to each delivery attempt, on every request. Store it and check for it before processing:
async function processShopifyWebhook(topic: string, payload: unknown, deliveryId: string) {
const seen = await db.processedWebhooks.findUnique({ where: { deliveryId } });
if (seen) return; // Already handled, e.g. a retry after a timeout
await db.processedWebhooks.create({ data: { deliveryId, topic } });
// ... handle the event
}
Retries during the 4-hour window will reuse the same delivery ID, so this catches the most common source of duplicate processing without needing to hash the payload.
Compliance webhooks: mandatory for public apps
If you're building a public app for the Shopify App Store, three topics are non-negotiable regardless of what your app actually does:
customers/data_request- a merchant's customer has requested their data; you must respond with what you hold on themcustomers/redact- a merchant's customer has requested deletion; you must erase their data within 30 daysshop/redact- a merchant has uninstalled your app and 48 hours have passed; erase their shop's data
Shopify sends these automatically to every registered app; you don't request them. App review checks that all three return a 200 and are handled correctly, even for an app that stores nothing about individual customers. In that case, the handler can be a no-op that still returns 200, but it has to exist.
Common failure patterns
"HMAC verification passes in Postman but fails from Shopify"
Almost always a body-parsing issue: something in the request pipeline (a proxy, a global JSON middleware) is altering the bytes before your verification code sees them. Use a raw body parser scoped to the webhook route only.
"Webhook fires once per order but I'm seeing it twice"
Check whether you're deduplicating on X-Shopify-Webhook-Id. A retry after a slow response looks, from your database's perspective, exactly like a second genuine order unless you're checking the delivery ID first.
"App review rejected for compliance webhooks"
The most common cause is one of the three topics returning a non-200, often because the handler throws on shops that have no data to redact. Make sure all three succeed even in the empty case.
Shopify's per-topic subscriptions and base64 HMAC are the two things that catch people coming from other providers. Once those are handled, the rest (fast acknowledgment, async processing, dedup on delivery ID) is the same pattern that works for any webhook source.
If you're connecting Shopify to Webhook Watchtower, the Shopify setup guide covers the built-in preset: it selects the base64 HMAC algorithm automatically and deduplicates on X-Shopify-Webhook-Id, so a retried delivery never reaches your endpoint twice.
Frequently asked questions
How do I verify a Shopify webhook is genuine?
Compute an HMAC-SHA256 hash of the raw request body using your app's client secret, base64-encode it, and compare against the X-Shopify-Hmac-SHA256 header using a constant-time comparison. Shopify's encoding is base64, not hex, which trips up anyone reusing verification code written for Stripe or GitHub.
Why do I need one webhook subscription per topic in Shopify?
Unlike GitHub, where one webhook can cover many event types, Shopify requires a separate subscription for each topic: orders/create, products/update, and so on. You can point every subscription at the same endpoint URL, and route based on the X-Shopify-Topic header once requests arrive.
What are Shopify's mandatory compliance webhooks?
Any public Shopify app must implement three GDPR-related webhook topics: customers/data_request, customers/redact, and shop/redact. Shopify sends these automatically, and app review will reject a submission that doesn't handle all three correctly, even if your app doesn't otherwise store customer data.
Share this post
Webhook Watchtower
Stop debugging webhooks in logs.
Capture every event, replay failures, and get alerted when deliveries fail, without changing your existing setup.
Try it free →Related posts
Monitoring Webhook Health: The Metrics That Actually Matter
Uptime doesn't tell you whether webhooks are working. A short list of the signals that actually indicate a healthy webhook pipeline, and what each one catches that the others miss.
Alerting on Webhook Failures Without Alert Fatigue
Notify on every failed delivery attempt and you'll train people to ignore the channel. A pattern for alerting on what actually needs a human, and staying quiet about what doesn't.
IP Allowlisting vs Signature Verification: What Actually Secures a Webhook Endpoint
IP allowlisting feels like a solid first line of defence for a webhook endpoint. It isn't, and understanding why makes the case for signature verification much clearer.