This guide covers webhook registration, validation, and processing using the Shopify API library.
Overview
The library supports multiple webhook delivery methods:
- HTTP - Webhooks delivered to your app’s endpoint
- EventBridge - AWS EventBridge delivery
- PubSub - Google Cloud Pub/Sub delivery
For most apps, we recommend using app-specific webhooks configured in your shopify.app.toml file instead of registering webhooks programmatically. Only use webhooks.addHandlers and webhooks.register if you need shop-specific webhook configurations.
Adding Webhook Handlers
Register webhook handlers when initializing your app:
import {shopifyApi, DeliveryMethod} from '@shopify/shopify-api';
const shopify = shopifyApi({...});
shopify.webhooks.addHandlers({
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products/create',
callback: async (topic, shop, body, webhookId, apiVersion, subTopic) => {
const payload = JSON.parse(body);
console.log('Product created:', payload.id);
},
},
ORDERS_PAID: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/orders/paid',
callback: async (topic, shop, body) => {
const order = JSON.parse(body);
// Process paid order
},
},
});
Source: lib/webhooks/registry.ts:20-37
Delivery Methods
{
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products/create',
callback: async (topic, shop, body, webhookId, apiVersion) => {
// Handle webhook
},
}
The callbackUrl can be:
- Relative path:
/webhooks/products/create (uses configured host)
- Absolute URL:
https://example.com/webhooks/products/create
Source: lib/webhooks/registry.ts:71-80{
deliveryMethod: DeliveryMethod.EventBridge,
arn: 'arn:aws:events:us-east-1:123456789012:event-bus/my-bus',
}
Events are sent to your AWS EventBridge event bus.{
deliveryMethod: DeliveryMethod.PubSub,
pubSubProject: 'my-project-id',
pubSubTopic: 'shopify-webhooks',
}
Events are published to your Google Cloud Pub/Sub topic.
Registering Webhooks
After adding handlers, register them with Shopify:
app.get('/webhooks/register', async (req, res) => {
const session = await loadSession(req);
const response = await shopify.webhooks.register({session});
console.log('Registration results:', response);
res.json(response);
});
Registration Response:
interface RegisterReturn {
[topic: string]: RegisterResult[];
}
interface RegisterResult {
deliveryMethod: DeliveryMethod;
success: boolean;
result: any;
operation: 'create' | 'update' | 'delete';
}
Source: lib/webhooks/register.ts:46-112
The register function automatically:
- Creates new webhook subscriptions
- Updates existing subscriptions if fields changed
- Deletes subscriptions that are no longer needed
Source: lib/webhooks/register.ts:233-281
Validating Webhooks
Always validate incoming webhooks to ensure they’re from Shopify:
app.post('/webhooks/products/create', async (req, res) => {
const rawBody = await getRawBody(req); // Get raw request body
const validationResult = await shopify.webhooks.validate({
rawBody,
rawRequest: req,
rawResponse: res,
});
if (!validationResult.valid) {
console.error('Invalid webhook:', validationResult.reason);
return res.status(401).send('Unauthorized');
}
// Webhook is valid
console.log('Topic:', validationResult.topic);
console.log('Shop:', validationResult.domain);
console.log('Webhook ID:', validationResult.webhookId);
res.status(200).send('OK');
});
Validation Response:
interface WebhookValidationValid {
valid: true;
webhookType: 'webhooks' | 'events';
topic: string;
domain: string;
apiVersion: string;
webhookId: string;
hmac: string;
}
interface WebhookValidationInvalid {
valid: false;
reason: 'missing_headers' | 'missing_body' | 'invalid_hmac';
missingHeaders?: string[];
}
Source: lib/webhooks/validate.ts:46-75
Validation Process
The library validates:
- Required headers -
X-Shopify-Hmac-SHA256, X-Shopify-Topic, etc.
- HMAC signature - Verifies the webhook is from Shopify
- Request body - Ensures body is present
Source: lib/webhooks/validate.ts:89-149
Important: Webhooks manually triggered from the Shopify admin will fail HMAC validation. Use the Shopify CLI or real events to test webhooks in development.Source: lib/webhooks/validate.ts:64-69
Processing Webhooks
The process function validates and calls your webhook handlers automatically:
app.post('/webhooks/*', async (req, res) => {
const rawBody = await getRawBody(req);
try {
await shopify.webhooks.process({
rawBody,
rawRequest: req,
rawResponse: res,
context: {userId: req.user?.id}, // Optional context
});
} catch (error) {
console.error('Webhook processing failed:', error);
return res.status(500).send('Error');
}
});
How it works:
- Validates the webhook HMAC and headers
- Extracts the topic from headers
- Finds matching handlers for the topic
- Calls each handler’s
callback function
- Returns appropriate HTTP status codes
Source: lib/webhooks/process.ts:43-97
Handler Callback Signature
type WebhookCallback = (
topic: string,
shop: string,
body: string, // Raw JSON string
webhookId: string,
apiVersion: string,
subTopic?: string,
context?: any, // Optional context from process()
) => Promise<void>;
Source: lib/webhooks/process.ts:99-172
Advanced Options
Include Fields
Limit webhook payload to specific fields:
shopify.webhooks.addHandlers({
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products/update',
includeFields: ['id', 'title', 'variants'],
callback: async (topic, shop, body) => {
// Body only contains id, title, and variants
},
},
});
Include specific metafield namespaces in the webhook:
shopify.webhooks.addHandlers({
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products/create',
metafieldNamespaces: ['custom', 'inventory'],
callback: async (topic, shop, body) => {
// Includes metafields from 'custom' and 'inventory' namespaces
},
},
});
Source: lib/webhooks/registry.ts:90-92
Privacy Webhooks
Privacy webhooks (GDPR) must be handled separately:
const PRIVACY_TOPICS = [
'CUSTOMERS_DATA_REQUEST',
'CUSTOMERS_REDACT',
'SHOP_REDACT',
];
app.post('/webhooks/gdpr/:topic', async (req, res) => {
const rawBody = await getRawBody(req);
const topic = req.params.topic.toUpperCase();
const validation = await shopify.webhooks.validate({
rawBody,
rawRequest: req,
rawResponse: res,
});
if (!validation.valid) {
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(rawBody);
switch (topic) {
case 'CUSTOMERS_DATA_REQUEST':
// Return customer data
break;
case 'CUSTOMERS_REDACT':
// Delete customer data
break;
case 'SHOP_REDACT':
// Delete shop data (48 hours after uninstall)
break;
}
res.status(200).send('OK');
});
Privacy webhooks are automatically skipped during webhooks.register() because they must be configured in the Partner Dashboard.Source: lib/webhooks/register.ts:76-78
Error Handling
import {InvalidWebhookError} from '@shopify/shopify-api';
try {
await shopify.webhooks.process({...});
} catch (error) {
if (error instanceof InvalidWebhookError) {
console.error('Webhook error:', error.message);
console.error('Response:', error.response);
}
}
Source: lib/error.ts:99-110
Complete Example
import express from 'express';
import {shopifyApi, DeliveryMethod} from '@shopify/shopify-api';
const shopify = shopifyApi({...});
const app = express();
// Add webhook handlers
shopify.webhooks.addHandlers({
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products/create',
callback: productCreateHandler,
},
ORDERS_PAID: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/orders/paid',
callback: orderPaidHandler,
},
});
async function productCreateHandler(topic, shop, body) {
const product = JSON.parse(body);
console.log(`New product ${product.id} in ${shop}`);
}
async function orderPaidHandler(topic, shop, body) {
const order = JSON.parse(body);
console.log(`Order ${order.id} paid in ${shop}`);
}
Best Practices
- Always validate webhooks before processing
- Return 200 OK immediately, process asynchronously
- Store raw body for validation - don’t parse before validating
- Use app-specific webhooks (shopify.app.toml) when possible
- Handle webhook retries idempotently