Shopify Webhooks: A Complete Guide for Developers
A comprehensive, production-tested guide to Shopify webhooks: HMAC signature verification, raw body handling, idempotency with Redis, queue architecture, and local testing.

Yash Nandvana
Full Stack Developer

1. Introduction
In modern e-commerce engineering, state changes occur constantly. Orders are placed, customer profiles are updated, inventory counts fluctuate across fulfillment centers, and merchants uninstall applications.
For developers building Shopify apps or custom integrations, discovering these state changes efficiently is a fundamental architectural requirement.
If your application polls Shopify's Admin API every 30 seconds asking "Has anything changed?", you encounter immediate scaling bottlenecks:
To solve this, Shopify provides Webhooks—an event-driven notification mechanism where Shopify pushes data to your HTTP server instantly whenever a relevant event occurs.
2. What Are Shopify Webhooks?
A Shopify webhook is an automated HTTP POST request sent from Shopify's servers to a designated HTTPS URL (your webhook endpoint) when a specific event takes place within a Shopify store.
Webhooks operate on a Publisher/Subscriber model:
orders/create) and provides a secure HTTPS callback URL.POST payload containing detailed JSON representation of the event to your endpoint.Real-World Use Cases
inventory_levels/update webhook fires.orders/paid.customers/redact and shop/redact.app/uninstalled.3. High-Level Event-Driven Architecture
In production, handling webhooks synchronously inside your web server route is a major architectural anti-pattern. Shopify expects your server to respond with an HTTP 200 OK status within 5 seconds. If your endpoint takes longer or crashes, Shopify considers the delivery failed and will attempt retries with exponential backoff.
Recommended Asynchronous Architecture
┌─────────────────┐ HTTP POST Payload ┌────────────────────────┐
│ Shopify Store │ ─────────────────────────────> │ API Gateway / Router │
└─────────────────┘ (HMAC Signature Header) └────────────────────────┘
│
│ 1. Fast Verification
│ 2. Enqueue Job
▼
┌─────────────────┐ Async Job Process ┌────────────────────────┐
│ Worker Process │ <───────────────────────────── │ Redis / BullMQ Queue │
│ (DB Updates) │ └────────────────────────┘
└─────────────────┘200 OK.4. Webhook Registration Methods
Shopify allows registering webhooks through three distinct mechanisms:
Method 1 — GraphQL Admin API (Recommended)
Registering webhooks programmatically via the GraphQL Admin API allows dynamic endpoint configuration upon app installation.
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
userErrors {
field
message
}
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
}
}Variables Payload:
{
"topic": "ORDERS_CREATE",
"webhookSubscription": {
"callbackUrl": "https://api.myapp.com/webhooks/orders-create",
"format": "JSON"
}
}Method 2 — Shopify CLI App Configuration (shopify.app.toml)
For modern Shopify apps built with the Remix or Node app templates, webhooks can be declared declaratively in shopify.app.toml:
[[webhooks.subscriptions]]
topics = [ "orders/create", "orders/edited" ]
uri = "/api/webhooks"5. Security & HMAC Verification
Because webhook endpoints are public HTTPS URLs, never trust incoming requests without verifying their origin. Malicious actors could send forged payloads to corrupt your database.
Shopify signs every webhook payload with a cryptographic HMAC-SHA256 hash generated using your app's Client Secret.
HTTP Headers Included by Shopify
X-Shopify-Hmac-Sha256: Base64-encoded HMAC-SHA256 signature string.X-Shopify-Topic: The event topic (e.g. orders/create).X-Shopify-Shop-Domain: The merchant's myshopify.com domain handle.X-Shopify-Webhook-Id: Unique UUID identifying the webhook event.Verifying HMAC Signature in Node.js / Express
CRITICAL REQUIREMENT: HMAC calculation MUST use the raw unparsed request body string or buffer. If body-parser middleware has already transformed the request into a JSON object, stringifying it again will reorder keys and cause verification to fail!
import crypto from 'crypto';
export function verifyShopifyHmac(req, res, next) {
const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
const clientSecret = process.env.SHOPIFY_API_SECRET;
if (!hmacHeader) {
return res.status(401).send('Missing HMAC header');
}
// req.rawBody must be populated by express.raw() or a custom verify function
const calculatedHmac = crypto
.createHmac('sha256', clientSecret)
.update(req.rawBody, 'utf8')
.digest('base64');
const hmacValid = crypto.timingSafeEqual(
Buffer.from(calculatedHmac),
Buffer.from(hmacHeader)
);
if (!hmacValid) {
console.error('HMAC Verification Failed!');
return res.status(401).send('Unauthorized webhook source');
}
return next();
}6. Preserving Raw Body in Express Middleware
To ensure req.rawBody is preserved while still supporting JSON bodies elsewhere:
import express from 'express';
const app = express();
// Capture raw body specifically for webhook routes
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
},
})
);7. Handling Idempotency & Retries
Network fluctuations or server restarts can cause Shopify to deliver the same webhook multiple times. If your application processes an orders/paid event twice without idempotency protection, you risk double-shipping or duplicate billing.
Idempotency Strategy using Redis
Shopify sends a unique X-Shopify-Webhook-Id with every payload. Before processing a payload, check whether the ID has already been recorded in Redis:
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export async function processWebhook(webhookId, payload) {
const redisKey = `webhook:processed:${webhookId}`;
// Set key only if it doesn't exist (NX), with a 24-hour expiration (86400s)
const isNew = await redis.set(redisKey, '1', 'EX', 86400, 'NX');
if (!isNew) {
console.log(`Duplicate webhook ${webhookId} ignored.`);
return { status: 'duplicate' };
}
// Execute actual business logic here
await handleOrderCreated(payload);
return { status: 'processed' };
}8. Local Testing & Webhook Debugging
During local development, your local Node server (localhost:3000) cannot receive HTTP calls directly from Shopify's public servers.
Recommended Local Workflow
shopify app dev which automatically provisions a secure Cloudflare tunnel to your dev server. shopify app webhook trigger --topic=ORDERS_CREATE --address=https://your-tunnel-url/api/webhooks9. Webhook Security & Production Checklist
200 OK in under 5 seconds.X-Shopify-Webhook-Id.customers/redact, shop/redact).crypto.timingSafeEqual) used for HMAC strings to prevent timing attacks.10. Conclusion
Mastering Shopify webhooks is essential for building robust, event-driven Shopify applications. By combining HMAC verification, raw body preservation, queue-based async processing, and idempotency tracking, you ensure your app scales safely in production.
Next Recommended Reads:

Yash Nandvana• Full Stack Developer
Full Stack & Shopify Developer building scalable web apps, developer tools, and AI solutions.
Learn more about YashRelated Articles
Shopify GraphQL Admin API: A Practical Guide for Developers
A comprehensive, production-oriented guide to Shopify's GraphQL Admin API: queries, mutations, pagination, rate limit cost calculation, userErrors checking, and bulk operations.
Building a Production-Ready REST API with Node.js, PostgreSQL & Prisma
A comprehensive architectural guide to building production Node.js backends: layer separation, Prisma ORM, PostgreSQL database design, JWT auth, input validation, and security.
AI Coding Agents in 2026: How Developers Actually Use Claude Code, Codex & Copilot
A practical engineering guide to AI coding agents in 2026 — how they differ from autocomplete, how to use Claude Code, Codex, and GitHub Copilot in real workflows, and why human supervision still matters.
