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.
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! 🙌
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
- 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 Webhook Headers Reference
Shopify includes several headers with every webhook request. These headers are essential for verification, deduplication, and routing:
| Header | Description | Example Value |
|---|---|---|
X-Shopify-Topic | The webhook event type | orders/create |
X-Shopify-Hmac-SHA256 | Base64-encoded HMAC signature for verification | XfH3...8Q== |
X-Shopify-Shop-Domain | The shop's myshopify.com domain | mystore.myshopify.com |
X-Shopify-Webhook-Id | Unique ID for the webhook subscription | b123456-... |
X-Shopify-Event-Id | Unique ID for this specific event (use for deduplication) | e789012-... |
X-Shopify-Triggered-At | ISO 8601 timestamp when the event was triggered | 2024-01-15T10:30:00.000Z |
X-Shopify-API-Version | API version used for the payload | 2024-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:
| Field | Description | Notes |
|---|---|---|
email | Customer's email address | Available if customer entered email or is logged in |
customer.id | Shopify customer ID | Only present if customer is logged in |
abandoned_checkout_url | Recovery URL to send to customer | Use in abandoned cart emails |
completed_at | Null for active checkouts | Becomes timestamp when order is placed |
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:
| Field | Description | Notes |
|---|---|---|
customer_id | Shopify customer ID | Only present if customer is logged in |
token | Cart token | Use to track cart across sessions |
line_items | Products in cart | Array of items with product details |
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:
| Field | Description | Notes |
|---|---|---|
inventory_item_id | ID of the inventory item | Links to product variant |
location_id | ID of the inventory location | Warehouse or store location |
available | Current available quantity | After 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:
- Go to Shopify Admin → Settings → Notifications → Webhooks
- Check the status indicator next to each webhook
- Failed webhooks show error counts and last failure time
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.
Using webhook-verify (Recommended)
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 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 | Easy with webhook-verify + req.rawBody |
| 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
- webhook-verify on npm - Unified webhook signature verification for 20+ providers
- 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
- What are Webhooks? - Learn webhook fundamentals
- Stripe Webhooks Example - Similar payment webhook handler
- Browse All Webhook Examples - Explore all webhook integration examples
- Quick Deploy Templates - Ready-to-deploy backend templates
Shopify Webhooks Integration FAQ
Common questions about Shopify webhook integration
How do I verify Shopify webhook signatures?
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?
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.How quickly must I respond to Shopify webhooks?
Can I filter which webhooks I receive?
Does the checkouts/create webhook include customer email?
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?
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?
What headers does Shopify send with webhooks?
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?
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.