Skip to main content

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.

Use the Template

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! 🙌
Finding your API endpoint

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

  1. Go to Settings → Notifications → Webhooks
  2. Click Create webhook
  3. Select event (e.g., "Order creation")
  4. Enter your Codehooks URL
  5. Select format: JSON
  6. 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

TopicDescriptionCommon Use Case
orders/createNew order placedFulfillment, inventory sync
orders/updatedOrder modifiedStatus updates
orders/fulfilledOrder shippedShipping notifications
orders/cancelledOrder canceledRefund processing
products/createNew product addedCatalog sync
products/updateProduct modifiedSearch index update
products/deleteProduct removedCleanup external systems
inventory_levels/updateStock changedLow stock alerts
customers/createNew customerCRM sync, welcome email
fulfillments/createFulfillment createdTracking updates
refunds/createRefund issuedAccounting sync
checkouts/createCheckout startedAbandoned 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 TopicWhen It FiresYour Responsibility
customers/data_requestCustomer requests their dataReturn all stored data within 30 days
customers/redactCustomer requests deletionDelete all personal data
shop/redact48 hours after app uninstallDelete all shop data
app/uninstalledApp is uninstalledClean up, revoke access
Required for App Store

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?

FeatureDIY SetupCodehooks.io
Setup timeHours/Days5 minutes
HMAC verificationManual implementationBuilt-in support
ScalingConfigure infrastructureAutomatic
Database for dedupSet up separatelyBuilt-in
TemplatesBuild from scratchReady-to-use
CostServer costsFree tier available

Shopify Webhooks FAQ

Common questions about handling Shopify webhooks

How do I verify Shopify webhook signatures?
Shopify sends an HMAC-SHA256 signature in the 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?
Common causes: (1) Using parsed JSON body instead of raw body - use 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?
Use the 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?
Shopify retries failed webhooks for up to 48 hours with exponential backoff. After 19 consecutive failures, Shopify automatically removes the webhook subscription. Monitor your webhook health in Shopify Admin → Notifications → Webhooks.
How do I register webhooks programmatically?
Use the Shopify Admin REST API: POST to /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?
Apps in the Shopify App Store must handle: 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.
How quickly must I respond to Shopify webhooks?
Return a 2xx response within 5 seconds. For heavy processing, acknowledge immediately with 200, then process asynchronously using a queue. Codehooks.io queues are perfect for this pattern.
Can I filter which webhooks I receive?
Yes, subscribe only to the topics you need. You can also use metafield-based filtering for some topics. Each webhook subscription is for a specific topic - you can't filter by payload content at the Shopify level, but you can filter in your handler code.