How to Receive Shopify Webhooks with Examples
Shopify webhooks notify your application in real-time when events occur in a store — orders are created, products are updated, inventory changes, and more. Instead of polling the Shopify API, webhooks push data to your app instantly.
The problem? Setting up Shopify webhooks requires HMAC verification, handling duplicate events, managing webhook subscriptions, and ensuring reliability.
The solution? Deploy a production-ready Shopify webhook handler with Codehooks.io in under 5 minutes using our ready-made template.
Prerequisites
Before you begin, sign up for a free Codehooks.io account and install the CLI:
npm install -g codehooks
Quick Deploy with Template
The fastest way to get started is using the Codehooks Shopify webhook template:
coho create myshopifyhandler --template webhook-shopify-minimal
You'll see output like:
Creating project: myshopifyhandler
Project created successfully!
Your API endpoints:
https://myshopifyhandler-a1b2.api.codehooks.io/dev
Then install, deploy, and configure:
cd myshopifyhandler
npm install
coho deploy
coho set-env SHOPIFY_WEBHOOK_SECRET your-webhook-secret --encrypted
That's it! The template includes HMAC verification, event handling, and database storage out of the box.
The webhook-shopify-minimal template is production-ready with proper HMAC verification, error handling, and event storage. Browse all available templates in the templates repository.
Custom Implementation
If you prefer to build from scratch, create a new project and add your Shopify webhook handler code to index.js:
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
import crypto from 'crypto';
const SHOPIFY_WEBHOOK_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET;
// Verify Shopify HMAC signature
function verifyShopifyWebhook(req) {
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody;
const calculatedHmac = crypto
.createHmac('sha256', SHOPIFY_WEBHOOK_SECRET)
.update(body, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(hmacHeader),
Buffer.from(calculatedHmac)
);
}
// Shopify webhook endpoint
app.post('/shopify/webhooks', async (req, res) => {
// Verify the webhook signature
if (!verifyShopifyWebhook(req)) {
console.error('Invalid Shopify webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const topic = req.headers['x-shopify-topic'];
const shop = req.headers['x-shopify-shop-domain'];
const eventId = req.headers['x-shopify-event-id'];
const payload = req.body;
const conn = await Datastore.open();
// Check for duplicate events
const existing = await conn.getOne('shopify_events', eventId);
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.json({ received: true, duplicate: true });
}
// Store the event
await conn.insertOne('shopify_events', {
_id: eventId,
topic,
shop,
payload,
processedAt: new Date().toISOString()
});
// Handle different webhook topics
switch (topic) {
case 'orders/create':
await handleOrderCreated(payload, shop);
break;
case 'orders/fulfilled':
await handleOrderFulfilled(payload, shop);
break;
case 'products/update':
await handleProductUpdated(payload, shop);
break;
case 'inventory_levels/update':
await handleInventoryUpdate(payload, shop);
break;
case 'customers/create':
await handleCustomerCreated(payload, shop);
break;
default:
console.log(`Unhandled topic: ${topic}`);
}
res.json({ received: true });
});
async function handleOrderCreated(order, shop) {
console.log(`New order ${order.id} from ${shop}`);
console.log(`Total: ${order.total_price} ${order.currency}`);
console.log(`Customer: ${order.customer?.email}`);
// Your business logic here:
// - Send to fulfillment system
// - Update inventory
// - Send confirmation email
// - Notify warehouse
}
async function handleOrderFulfilled(order, shop) {
console.log(`Order ${order.id} fulfilled`);
// Your business logic:
// - Send shipping notification
// - Update CRM
}
async function handleProductUpdated(product, shop) {
console.log(`Product ${product.id} updated: ${product.title}`);
// Your business logic:
// - Sync to external catalog
// - Update search index
}
async function handleInventoryUpdate(inventory, shop) {
console.log(`Inventory updated for item ${inventory.inventory_item_id}`);
console.log(`New quantity: ${inventory.available}`);
// Your business logic:
// - Alert if low stock
// - Update external systems
}
async function handleCustomerCreated(customer, shop) {
console.log(`New customer: ${customer.email}`);
// Your business logic:
// - Add to email list
// - Create CRM record
}
export default app.init();
Deploy:
coho deploy
Project: myshopifyhandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
Run coho info to see your API endpoints and tokens. Your webhook URL will be:
https://myshopifyhandler-a1b2.api.codehooks.io/dev/shopify/webhooks
Configure Shopify Webhooks
Option 1: Via Shopify Admin
- Go to Settings → Notifications → Webhooks
- Click Create webhook
- Select event (e.g., "Order creation")
- Enter your Codehooks URL
- Select format: JSON
- Save
Option 2: Via Shopify API
// Register a webhook programmatically
const response = await fetch(
`https://${shop}/admin/api/2024-01/webhooks.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhook: {
topic: 'orders/create',
address: 'https://your-app.api.codehooks.io/dev/shopify/webhooks',
format: 'json'
}
})
}
);
Set Environment Variables
coho set-env SHOPIFY_WEBHOOK_SECRET your-webhook-secret --encrypted
Find your webhook secret in Shopify Admin → Settings → Notifications → Webhooks (at the bottom of the page).
Common Shopify Webhook Topics
| Topic | Description | Common Use Case |
|---|---|---|
orders/create | New order placed | Fulfillment, inventory sync |
orders/updated | Order modified | Status updates |
orders/fulfilled | Order shipped | Shipping notifications |
orders/cancelled | Order canceled | Refund processing |
products/create | New product added | Catalog sync |
products/update | Product modified | Search index update |
products/delete | Product removed | Cleanup external systems |
inventory_levels/update | Stock changed | Low stock alerts |
customers/create | New customer | CRM sync, welcome email |
fulfillments/create | Fulfillment created | Tracking updates |
refunds/create | Refund issued | Accounting sync |
checkouts/create | Checkout started | Abandoned cart tracking |
Shopify HMAC Verification
Shopify signs all webhook payloads with an HMAC-SHA256 signature. Always verify this signature:
import crypto from 'crypto';
function verifyShopifyWebhook(req) {
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
// Must use raw body, not parsed JSON
const body = req.rawBody;
const calculatedHmac = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(hmacHeader),
Buffer.from(calculatedHmac)
);
} catch (e) {
return false;
}
}
Important: Use req.rawBody (the raw string), not req.body (parsed JSON). The signature is computed over the exact bytes Shopify sent.
Handling Duplicate Events
Shopify may send the same webhook multiple times. Use the X-Shopify-Event-Id header for idempotency:
import { Datastore } from 'codehooks-js';
app.post('/shopify/webhooks', async (req, res) => {
const eventId = req.headers['x-shopify-event-id'];
const conn = await Datastore.open();
// Check if we've already processed this event
const existing = await conn.getOne('processed_events', eventId);
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.json({ received: true });
}
// Process the event
await processWebhook(req.body);
// Mark as processed
await conn.insertOne('processed_events', {
_id: eventId,
topic: req.headers['x-shopify-topic'],
processedAt: new Date().toISOString()
});
res.json({ received: true });
});
Best Practices for Shopify Webhooks
1. Respond Quickly
Shopify expects a response within 5 seconds. Process heavy logic asynchronously:
app.post('/shopify/webhooks', async (req, res) => {
// Verify signature first
if (!verifyShopifyWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Return 200 immediately
res.json({ received: true });
// Process asynchronously
await queueWebhookProcessing(req.body, req.headers);
});
2. Handle Event Ordering
Events may arrive out of order. Use timestamps:
const triggeredAt = req.headers['x-shopify-triggered-at'];
const payloadUpdatedAt = req.body.updated_at;
// Compare with stored timestamp before updating
3. Handle Mandatory Compliance Webhooks (GDPR)
If your app is in the Shopify App Store, you must handle these mandatory webhooks for GDPR/privacy compliance:
// Mandatory webhook: Customer requests their data
app.post('/shopify/customers/data_request', async (req, res) => {
if (!verifyShopifyWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { customer, shop_domain } = req.body;
console.log(`Data request from ${customer.email} for shop ${shop_domain}`);
// Return all stored customer data
// You have 30 days to respond
res.json({ received: true });
});
// Mandatory webhook: Erase customer data (GDPR right to be forgotten)
app.post('/shopify/customers/redact', async (req, res) => {
if (!verifyShopifyWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { customer, shop_domain } = req.body;
const conn = await Datastore.open();
// Delete all customer data from your database
await conn.removeMany('customers', { email: customer.email });
await conn.removeMany('orders', { customerId: customer.id });
console.log(`Erased data for customer ${customer.id}`);
res.json({ received: true });
});
// Mandatory webhook: Erase shop data when app is uninstalled
app.post('/shopify/shop/redact', async (req, res) => {
if (!verifyShopifyWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { shop_domain } = req.body;
const conn = await Datastore.open();
// Delete all data for this shop
await conn.removeMany('shop_data', { shop: shop_domain });
console.log(`Erased all data for shop ${shop_domain}`);
res.json({ received: true });
});
| Mandatory Topic | When It Fires | Your Responsibility |
|---|---|---|
customers/data_request | Customer requests their data | Return all stored data within 30 days |
customers/redact | Customer requests deletion | Delete all personal data |
shop/redact | 48 hours after app uninstall | Delete all shop data |
app/uninstalled | App is uninstalled | Clean up, revoke access |
Failure to implement these mandatory webhooks will result in your app being rejected from the Shopify App Store.
Why Use Codehooks.io for Shopify Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---|---|---|
| Setup time | Hours/Days | 5 minutes |
| HMAC verification | Manual implementation | Built-in support |
| Scaling | Configure infrastructure | Automatic |
| Database for dedup | Set up separately | Built-in |
| Templates | Build from scratch | Ready-to-use |
| Cost | Server costs | Free tier available |
Related Resources
- Shopify Webhook Template - Ready-to-deploy template
- All Codehooks Templates - Browse all webhook templates
- Shopify Webhooks Documentation
- Shopify Webhook Topics Reference
- Codehooks.io Quick Start
- Webhook Delivery System - Build your own outgoing webhook system
Shopify Webhooks FAQ
Common questions about handling Shopify webhooks
How do I verify Shopify webhook signatures?
X-Shopify-Hmac-SHA256 header. Compute the HMAC of the raw request body using your webhook secret, base64 encode it, and compare using crypto.timingSafeEqual(). Find your secret in Shopify Admin → Settings → Notifications → Webhooks.Why am I getting 'Invalid signature' errors?
req.rawBody, (2) Wrong webhook secret - each app has a unique secret, (3) Secret has extra whitespace - trim it. Also ensure you're base64 encoding the HMAC digest.How do I handle duplicate Shopify webhooks?
X-Shopify-Event-Id header as a unique identifier. Store processed event IDs in your database and check before processing. This prevents duplicate order fulfillment or other side effects.What happens if my webhook endpoint is down?
How do I register webhooks programmatically?
/admin/api/2024-01/webhooks.json with the topic, address (your URL), and format (json). You need the write_webhooks scope. For apps, you can also configure webhooks in shopify.app.toml.What are mandatory webhooks for Shopify apps?
app/uninstalled (cleanup when uninstalled), customers/data_request (GDPR data request), customers/redact (delete customer data), and shop/redact (delete shop data). Failure to implement these can get your app rejected.