Skip to main content

Shopify Webhooks Integration Example: Handle Orders & Inventory Events

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 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 includes HMAC verification, error handling, and event storage to get you started quickly. 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, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';

// Allow webhook requests without JWT authentication
app.auth('/shopify/*', (req, res, next) => next());

// Shopify webhook endpoint
app.post('/shopify/webhooks', async (req, res) => {
// Verify the webhook signature
const isValid = verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET);
if (!isValid) {
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.findOneOrNull('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

Codehooks also provides alternative URLs without the space name (e.g., https://gracious-inlet-fd79.codehooks.io/shopify/webhooks). On paid plans, you can also use custom domains for production use.

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 Webhook Headers Reference

Shopify includes several headers with every webhook request. These headers are essential for verification, deduplication, and routing:

HeaderDescriptionExample Value
X-Shopify-TopicThe webhook event typeorders/create
X-Shopify-Hmac-SHA256Base64-encoded HMAC signature for verificationXfH3...8Q==
X-Shopify-Shop-DomainThe shop's myshopify.com domainmystore.myshopify.com
X-Shopify-Webhook-IdUnique ID for the webhook subscriptionb123456-...
X-Shopify-Event-IdUnique ID for this specific event (use for deduplication)e789012-...
X-Shopify-Triggered-AtISO 8601 timestamp when the event was triggered2024-01-15T10:30:00.000Z
X-Shopify-API-VersionAPI version used for the payload2024-01
import { app } from 'codehooks-js';

// Allow webhook requests without JWT authentication
app.auth('/shopify/*', (req, res, next) => next());

app.post('/shopify/webhooks', async (req, res) => {
const topic = req.headers['x-shopify-topic'];
const hmac = req.headers['x-shopify-hmac-sha256'];
const shop = req.headers['x-shopify-shop-domain'];
const webhookId = req.headers['x-shopify-webhook-id'];
const eventId = req.headers['x-shopify-event-id'];
const triggeredAt = req.headers['x-shopify-triggered-at'];

console.log(`Received ${topic} from ${shop} at ${triggeredAt}`);
// ...
});

export default app.init();

Webhook Payload Examples

orders/create Webhook Payload Example

The orders/create webhook fires when a customer completes checkout. Here's the payload structure:

{
"id": 5678901234567,
"email": "[email protected]",
"created_at": "2024-01-15T10:30:00-05:00",
"updated_at": "2024-01-15T10:30:00-05:00",
"number": 1234,
"order_number": 1234,
"total_price": "149.99",
"subtotal_price": "139.99",
"total_tax": "10.00",
"currency": "USD",
"financial_status": "paid",
"fulfillment_status": null,
"customer": {
"id": 1234567890123,
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"phone": "+1-555-123-4567",
"tags": "vip,returning",
"orders_count": 5,
"total_spent": "599.95"
},
"billing_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 2,
"price": "69.99",
"sku": "WIDGET-001",
"vendor": "Acme Co"
}
],
"shipping_lines": [
{
"title": "Standard Shipping",
"price": "9.99",
"code": "standard"
}
],
"discount_codes": [],
"note": "Please gift wrap",
"tags": ""
}

checkouts/create Webhook Payload Example

The checkouts/create webhook fires when a customer initiates checkout — useful for abandoned cart tracking and recovery campaigns.

{
"id": 9876543210987,
"token": "abc123def456",
"cart_token": "cart_789xyz",
"email": "[email protected]",
"created_at": "2024-01-15T10:25:00-05:00",
"updated_at": "2024-01-15T10:25:00-05:00",
"completed_at": null,
"currency": "USD",
"total_price": "149.99",
"subtotal_price": "139.99",
"total_tax": "10.00",
"customer": {
"id": 1234567890123,
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"phone": "+1-555-123-4567"
},
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"line_items": [
{
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 2,
"price": "69.99",
"sku": "WIDGET-001"
}
],
"abandoned_checkout_url": "https://mystore.myshopify.com/checkouts/abc123/recover"
}

Key fields in checkouts/create:

FieldDescriptionNotes
emailCustomer's email addressAvailable if customer entered email or is logged in
customer.idShopify customer IDOnly present if customer is logged in
abandoned_checkout_urlRecovery URL to send to customerUse in abandoned cart emails
completed_atNull for active checkoutsBecomes timestamp when order is placed
checkouts/create vs orders/create

Use checkouts/create for abandoned cart tracking — it fires when checkout begins. Use orders/create for fulfillment and order processing — it fires only when payment is complete.

carts/create Webhook Payload Example

The carts/create webhook fires when a customer adds their first item to cart — useful for early engagement tracking.

{
"id": "cart_abc123def456",
"token": "abc123def456",
"created_at": "2024-01-15T10:20:00-05:00",
"updated_at": "2024-01-15T10:20:00-05:00",
"currency": "USD",
"customer_id": 1234567890123,
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 1,
"price": "69.99",
"sku": "WIDGET-001",
"properties": {}
}
],
"note": ""
}

Key fields in carts/create:

FieldDescriptionNotes
customer_idShopify customer IDOnly present if customer is logged in
tokenCart tokenUse to track cart across sessions
line_itemsProducts in cartArray of items with product details
Customer identification in carts

The customer_id field is only included in carts/create and carts/update when the customer is logged into their account. For guest shoppers, you won't have customer identification until they enter their email at checkout.

carts/update Webhook Payload Example

The carts/update webhook fires when items are added, removed, or quantities change:

{
"id": "cart_abc123def456",
"token": "abc123def456",
"created_at": "2024-01-15T10:20:00-05:00",
"updated_at": "2024-01-15T10:22:00-05:00",
"currency": "USD",
"customer_id": 1234567890123,
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 3,
"price": "69.99",
"sku": "WIDGET-001"
},
{
"id": 9876543210988,
"product_id": 1111111111112,
"variant_id": 2222222222223,
"title": "Widget Accessory Pack",
"quantity": 1,
"price": "19.99",
"sku": "WIDGET-ACC-001"
}
],
"note": "Gift for a friend"
}

inventory_levels/update Webhook Payload Example

The inventory_levels/update webhook fires when inventory quantities change — essential for low-stock alerts and multi-channel inventory sync.

{
"inventory_item_id": 1234567890123,
"location_id": 9876543210987,
"available": 5,
"updated_at": "2024-01-15T10:35:00-05:00"
}

Key fields in inventory_levels/update:

FieldDescriptionNotes
inventory_item_idID of the inventory itemLinks to product variant
location_idID of the inventory locationWarehouse or store location
availableCurrent available quantityAfter the update

Example: Low stock alerts with Slack:

import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';

app.auth('/shopify/*', (req, res, next) => next());

app.post('/shopify/webhooks/inventory', async (req, res) => {
// Verify signature
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}

const { inventory_item_id, location_id, available } = req.body;
const conn = await Datastore.open();

// Low stock alert
if (available <= 5 && available > 0) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Low stock alert: Item ${inventory_item_id} has only ${available} units`
})
});
}

// Out of stock alert
if (available === 0) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Out of stock: Item ${inventory_item_id} at location ${location_id}`
})
});
}

// Store inventory level for tracking
await conn.updateOne('inventory_levels',
{ inventory_item_id, location_id },
{ $set: { available, updatedAt: new Date().toISOString() } },
{ upsert: true }
);

res.json({ received: true });
});

export default app.init();

Shopify Webhook Retry Policy

Shopify automatically retries failed webhook deliveries using exponential backoff:

  • Retry window: Up to 48 hours
  • Backoff schedule: Retries at increasing intervals (1 min, 2 min, 5 min, 10 min, etc.)
  • Failure threshold: After 19 consecutive failures, Shopify automatically removes the webhook subscription
  • Success response: Any 2xx status code (200, 201, 204, etc.)
  • Failure response: 4xx or 5xx status codes, or timeout (>5 seconds)

Monitoring webhook health:

  1. Go to Shopify Admin → Settings → Notifications → Webhooks
  2. Check the status indicator next to each webhook
  3. Failed webhooks show error counts and last failure time
Webhook removal after failures

If your endpoint returns errors 19 times in a row, Shopify will delete the webhook subscription entirely. You'll need to re-register it. Implement monitoring to catch issues before this happens.

Shopify HMAC Verification

Shopify signs all webhook payloads with an HMAC-SHA256 signature. Always verify this signature before processing.

The webhook-verify package provides a simple, unified API for verifying webhooks from 20+ providers including Shopify, Stripe, GitHub, Slack, and more:

npm install webhook-verify
import { verify } from 'webhook-verify';

app.post('/shopify/webhooks', async (req, res) => {
const isValid = verify(
'shopify',
req.rawBody,
req.headers,
process.env.SHOPIFY_WEBHOOK_SECRET
);

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process webhook...
});

The same package works for other webhook providers — just change the provider name:

// Stripe webhooks
verify('stripe', req.rawBody, req.headers, process.env.STRIPE_WEBHOOK_SECRET);

// GitHub webhooks
verify('github', req.rawBody, req.headers, process.env.GITHUB_WEBHOOK_SECRET);

// Slack webhooks
verify('slack', req.rawBody, req.headers, process.env.SLACK_SIGNING_SECRET);

Manual Verification

If you prefer to implement verification manually without dependencies:
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: Always 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 { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';

app.auth('/shopify/*', (req, res, next) => next());

app.post('/shopify/webhooks', async (req, res) => {
// Always verify first
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}

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.findOneOrNull('processed_events', eventId);
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.json({ received: true });
}

// Mark as processed first (before heavy work)
await conn.insertOne('processed_events', {
_id: eventId,
topic: req.headers['x-shopify-topic'],
processedAt: new Date().toISOString()
});

// Process the event (your business logic here)
const payload = req.body;
console.log(`Processing ${req.headers['x-shopify-topic']}:`, payload.id);

res.json({ received: true });
});

export default app.init();

Best Practices for Shopify Webhooks

1. Respond Quickly

Shopify expects a response within 5 seconds. Verify, store the event, and respond immediately — then process asynchronously:

import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';

app.auth('/shopify/*', (req, res, next) => next());

app.post('/shopify/webhooks', async (req, res) => {
// Verify signature first
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}

const conn = await Datastore.open();
const eventId = req.headers['x-shopify-event-id'];

// Check if we already have this event (idempotency)
const existing = await conn.findOneOrNull('shopify_events', eventId);
if (existing) {
return res.json({ received: true });
}

// Store event for async processing
await conn.insertOne('shopify_events', {
_id: eventId,
topic: req.headers['x-shopify-topic'],
shopDomain: req.headers['x-shopify-shop-domain'],
payload: req.body,
processedAt: null
});

// Respond immediately, process later via worker queue or cron job
res.json({ received: true });
});

export default app.init();

2. Handle Event Ordering

Events may arrive out of order. Use the updated_at timestamp from the payload to avoid overwriting newer data with older events:

const payloadUpdatedAt = new Date(req.body.updated_at);

// Fetch existing record
const existing = await conn.findOneOrNull('products', req.body.id);

// Only update if this event is newer than what we have
if (!existing || new Date(existing.updatedAt) < payloadUpdatedAt) {
await conn.updateOne('products', req.body.id, {
$set: { ...req.body, updatedAt: req.body.updated_at }
}, { upsert: true });
}

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:

import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';

app.auth('/shopify/*', (req, res, next) => next());

// Mandatory webhook: Customer requests their data
app.post('/shopify/customers/data_request', async (req, res) => {
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
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 (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
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 (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
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 });
});

export default app.init();
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 implementationEasy with webhook-verify + req.rawBody
ScalingConfigure infrastructureAutomatic
Database for dedupSet up separatelyBuilt-in
TemplatesBuild from scratchReady-to-use
CostServer costsFree tier available

Shopify Webhooks Integration FAQ

Common questions about Shopify webhook integration

How do I verify Shopify webhook signatures?
Use the webhook-verify package: verify('shopify', req.rawBody, req.headers, secret). It handles HMAC-SHA256 verification automatically and works with 20+ providers. 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.
Does the checkouts/create webhook include customer email?
Yes, the checkouts/create webhook includes the customer's email address when they've entered it or are logged into their account. The email appears in the email field at the root level and in customer.email if the customer object is present.
Does the carts/create webhook include customer_id?
The customer_id field is only included in carts/create when the customer is logged into their Shopify account. For guest shoppers, you won't have customer identification until they enter their email at checkout (via checkouts/create).
What is the Shopify webhook retry policy?
Shopify retries failed webhooks for up to 48 hours using exponential backoff. After 19 consecutive failures, Shopify automatically removes the webhook subscription. Always return a 2xx response within 5 seconds to avoid retries.
What headers does Shopify send with webhooks?
Shopify includes: X-Shopify-Topic (event type), X-Shopify-Hmac-SHA256 (signature), X-Shopify-Shop-Domain (store domain), X-Shopify-Webhook-Id (subscription ID), X-Shopify-Event-Id (unique event ID for deduplication), and X-Shopify-Triggered-At (timestamp).
What is the difference between checkouts/create and orders/create?
The checkouts/create webhook fires when a customer initiates checkout — use it for abandoned cart tracking. The orders/create webhook fires only when payment is complete — use it for fulfillment and order processing. A checkout may never become an order if the customer abandons it.