Webhooks are the backbone of Shopify automation. They enable real-time communication between your Shopify store and external systems — ERPs, warehouses, marketing platforms, analytics tools, and custom applications. Without webhooks, you would need to poll the Shopify API constantly to detect changes, which is inefficient, slow, and wasteful of your API rate limit budget.
This guide covers everything you need to build reliable, production-grade webhook consumers as part of your Shopify development stack. We have built webhook-driven integrations for dozens of stores, and the patterns here reflect the lessons we have learnt from handling millions of webhook deliveries.
What are Shopify webhooks
A webhook is an HTTP POST request that Shopify sends to a URL you specify when a specific event occurs. When a customer places an order, Shopify sends the order data to your endpoint within seconds. When a product is updated, Shopify notifies your system immediately. This push-based model is fundamentally more efficient than the pull-based model of polling the API.
Each webhook delivery includes the full resource data as a JSON payload in the request body, along with headers that identify the event type, the shop, and an HMAC signature for verification.
Webhooks are fire-and-forget from Shopify’s perspective. They send the data and expect a 200 response within 5 seconds. All processing must happen asynchronously after you acknowledge receipt.

Common webhook events
Shopify offers over 60 webhook topics. The most commonly used for ecommerce automation include:
- orders/create — triggered when a new order is placed. The most important webhook for ERP integration, fulfilment workflows, and analytics.
- orders/updated — triggered when any order attribute changes (payment status, fulfilment status, notes).
- orders/paid — triggered specifically when payment is captured. Useful for triggering fulfilment only after payment.
- products/update — triggered when product data changes (title, price, inventory). Essential for keeping external catalogues in sync.
- inventory_levels/update — triggered when inventory quantities change at a specific location.
- customers/create — triggered when a new customer account is created. Useful for CRM sync.
- app/uninstalled — triggered when your app is uninstalled. Use this for cleanup.
- shop/update — triggered when store settings change.
Registering webhooks via GraphQL
The recommended approach is registering webhooks via the GraphQL Admin API. This gives you programmatic control and works within custom apps:
mutation webhookSubscriptionCreate(
$topic: WebhookSubscriptionTopic!
$webhookSubscription: WebhookSubscriptionInput!
) {
webhookSubscriptionCreate(
topic: $topic
webhookSubscription: $webhookSubscription
) {
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}
// Variables
{
"topic": "ORDERS_CREATE",
"webhookSubscription": {
"callbackUrl": "https://your-app.com/webhooks/orders/create",
"format": "JSON"
}
}
Registering multiple webhooks at app installation
// Register all required webhooks after OAuth completes
const WEBHOOK_TOPICS = [
'ORDERS_CREATE',
'ORDERS_UPDATED',
'PRODUCTS_UPDATE',
'INVENTORY_LEVELS_UPDATE',
'CUSTOMERS_CREATE',
'APP_UNINSTALLED'
];
async function registerWebhooks(admin, appUrl) {
for (const topic of WEBHOOK_TOPICS) {
const topicSlug = topic.toLowerCase().replace('_', '-');
await admin.graphql(`
mutation {
webhookSubscriptionCreate(
topic: ${topic}
webhookSubscription: {
callbackUrl: "${appUrl}/webhooks/${topicSlug}"
format: JSON
}
) {
userErrors { field message }
}
}
`);
}
}
HMAC signature verification
Every webhook from Shopify includes an X-Shopify-Hmac-Sha256 header containing an HMAC-SHA256 digest of the request body, signed with your app’s secret key. You must verify this signature before processing any webhook. Without verification, any external party could send fake webhooks to your endpoint.
import crypto from 'crypto';
function verifyShopifyWebhook(req, res, next) {
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody; // Must be the raw request body, not parsed JSON
if (!hmacHeader || !body) {
return res.status(401).send('Unauthorized');
}
const generatedHmac = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET)
.update(body, 'utf8')
.digest('base64');
const verified = crypto.timingSafeEqual(
Buffer.from(generatedHmac),
Buffer.from(hmacHeader)
);
if (!verified) {
console.error('Webhook HMAC verification failed');
return res.status(401).send('Unauthorized');
}
next();
}
Critical: use crypto.timingSafeEqual for the comparison, not a simple string equality check. String comparison is vulnerable to timing attacks that can leak information about the expected HMAC.

Payload handling and processing
Webhook payloads contain the full resource data at the time of the event. Here is an example of an orders/create payload structure:
// Key fields in an orders/create webhook payload
{
"id": 820982911946154508,
"name": "#1001",
"email": "customer@example.com",
"created_at": "2026-03-16T09:30:00+00:00",
"financial_status": "paid",
"fulfillment_status": null,
"total_price": "99.95",
"currency": "GBP",
"line_items": [
{
"id": 866550311766439020,
"title": "Organic Cotton T-Shirt",
"variant_title": "Medium / Black",
"quantity": 2,
"price": "29.99",
"sku": "OCT-M-BLK"
}
],
"shipping_address": {
"first_name": "Jane",
"last_name": "Smith",
"address1": "123 High Street",
"city": "Bristol",
"province": "England",
"country": "United Kingdom",
"zip": "BS1 2AB"
}
}
Asynchronous processing patterns
Your webhook endpoint must respond with a 200 status within 5 seconds. If it takes longer, Shopify considers the delivery failed and will retry. This means all actual processing must happen asynchronously:
// Pattern 1: Queue-based processing (recommended)
import { Queue } from 'bullmq';
const webhookQueue = new Queue('shopify-webhooks');
app.post('/webhooks/orders/create', verifyShopifyWebhook, async (req, res) => {
// Acknowledge immediately
res.status(200).send();
// Queue for async processing
await webhookQueue.add('order-created', {
payload: req.body,
shopDomain: req.headers['x-shopify-shop-domain'],
webhookId: req.headers['x-shopify-webhook-id'],
timestamp: new Date().toISOString()
});
});
// Worker processes the queue
const worker = new Worker('shopify-webhooks', async (job) => {
switch (job.name) {
case 'order-created':
await processNewOrder(job.data.payload);
break;
}
});
Retry logic and failure handling
Shopify retries failed webhook deliveries with exponential backoff. The retry schedule spans up to 48 hours with 19 attempts. After all retries are exhausted, Shopify deletes the webhook subscription. Your system must be prepared for this:
- Always respond with 200 within 5 seconds, even if processing will happen later.
- Implement your own internal retry logic for processing failures.
- Set up monitoring to detect when webhook subscriptions are removed.
- Re-register webhooks automatically when your app detects missing subscriptions.
Idempotency and deduplication
Shopify may deliver the same webhook more than once. Your webhook handlers must be idempotent — safe to execute multiple times with the same input:
// Deduplication using webhook ID
const processedWebhooks = new Set(); // Use Redis in production
app.post('/webhooks/orders/create', verifyShopifyWebhook, async (req, res) => {
const webhookId = req.headers['x-shopify-webhook-id'];
// Check for duplicate delivery
if (await isAlreadyProcessed(webhookId)) {
console.log(`Duplicate webhook ${webhookId}, skipping`);
return res.status(200).send();
}
// Mark as processing
await markAsProcessed(webhookId);
// Acknowledge and queue
res.status(200).send();
await webhookQueue.add('order-created', { payload: req.body, webhookId });
});

Event-driven architecture patterns
For complex integrations, webhooks become the entry point for an event-driven architecture. A single webhook can trigger multiple downstream processes:
// Event fan-out pattern
async function processNewOrder(order) {
// Run multiple handlers concurrently
await Promise.allSettled([
syncOrderToERP(order), // Send to NetSuite/SAP
notifyWarehouse(order), // Trigger pick-and-pack
updateAnalytics(order), // Update dashboards
triggerPostPurchaseFlow(order), // Klaviyo post-purchase email
updateInventoryForecasts(order) // Adjust stock projections
]);
}
// Each handler is independent and fault-tolerant
async function syncOrderToERP(order) {
try {
await erpClient.createSalesOrder({
reference: order.name,
customer: order.email,
items: order.line_items.map(item => ({
sku: item.sku,
quantity: item.quantity,
price: item.price
})),
currency: order.currency,
total: order.total_price
});
} catch (error) {
// Log and retry, do not fail other handlers
await retryQueue.add('erp-sync', { order, error: error.message });
}
}
Monitoring and debugging
Webhook systems fail silently if you are not monitoring them. Set up alerting for:
- Delivery failures — track the ratio of 200 responses to non-200 responses.
- Processing latency — time from webhook receipt to processing completion.
- Queue depth — if the queue is growing faster than it drains, you have a throughput problem.
- Subscription status — periodically check that all expected webhook subscriptions are active.
- HMAC failures — a spike in verification failures may indicate a security issue or misconfigured secret.
// Health check: verify all webhook subscriptions are active
async function verifyWebhookSubscriptions(admin) {
const response = await admin.graphql(`
query {
webhookSubscriptions(first: 50) {
edges {
node {
id topic
endpoint {
... on WebhookHttpEndpoint { callbackUrl }
}
}
}
}
}
`);
const active = response.data.webhookSubscriptions.edges.map(e => e.node.topic);
const expected = ['ORDERS_CREATE', 'PRODUCTS_UPDATE', 'INVENTORY_LEVELS_UPDATE'];
const missing = expected.filter(t => !active.includes(t));
if (missing.length > 0) {
console.error('Missing webhook subscriptions:', missing);
// Re-register missing webhooks
for (const topic of missing) {
await registerWebhook(admin, topic);
}
}
}

Reliable webhook handling is the foundation of any serious Shopify integration. The patterns in this guide — HMAC verification, async processing, idempotency, and monitoring — are non-negotiable for production systems. If you need help building webhook-driven integrations for your Shopify store, get in touch — we build these systems as part of our Shopify development services.