E-commerce automation transforms manual order processing into scalable, error-free workflows. This guide covers building production-ready Shopify automations with n8n for order management, inventory control, and customer engagement.
Shopify Integration Setup
API Configuration
// Shopify API Configuration
// Store in n8n credentials
{
"shopifyApi": {
"apiKey": "{{ $env.SHOPIFY_API_KEY }}",
"password": "{{ $env.SHOPIFY_API_PASSWORD }}",
"shopSubdomain": "{{ $env.SHOPIFY_STORE_NAME }}",
"apiVersion": "2024-01"
}
}
// Required API Scopes:
// - read_orders, write_orders
// - read_products, write_products
// - read_inventory, write_inventory
// - read_customers, write_customers
// - read_fulfillments, write_fulfillmentsWebhook Configuration
// Function Node: Shopify Webhook Handler
const webhookData = $input.first().json;
const topic = $input.first().headers['x-shopify-topic'];
const hmac = $input.first().headers['x-shopify-hmac-sha256'];
// Verify webhook signature
const crypto = require('crypto');
const secret = $env.SHOPIFY_WEBHOOK_SECRET;
const body = JSON.stringify(webhookData);
const calculatedHmac = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
if (hmac !== calculatedHmac) {
throw new Error('Invalid webhook signature');
}
// Parse webhook topic
const [resource, action] = topic.split('/');
return [{
json: {
resource,
action,
data: webhookData,
receivedAt: new Date().toISOString(),
verified: true
}
}];Order Processing Workflows
New Order Handler
// Function Node: Process New Order
const order = $input.first().json.data;
// Extract order details
const orderData = {
orderId: order.id,
orderNumber: order.order_number,
orderName: order.name,
createdAt: order.created_at,
// Customer info
customer: {
id: order.customer?.id,
email: order.customer?.email || order.email,
firstName: order.customer?.first_name,
lastName: order.customer?.last_name,
phone: order.customer?.phone || order.phone,
totalOrders: order.customer?.orders_count || 1,
totalSpent: order.customer?.total_spent || order.total_price
},
// Order totals
financials: {
subtotal: parseFloat(order.subtotal_price),
totalTax: parseFloat(order.total_tax),
totalShipping: parseFloat(order.total_shipping_price_set?.shop_money?.amount || 0),
totalDiscount: parseFloat(order.total_discounts),
totalPrice: parseFloat(order.total_price),
currency: order.currency
},
// Line items
items: order.line_items.map(item => ({
id: item.id,
productId: item.product_id,
variantId: item.variant_id,
title: item.title,
variantTitle: item.variant_title,
sku: item.sku,
quantity: item.quantity,
price: parseFloat(item.price),
totalDiscount: parseFloat(item.total_discount),
requiresShipping: item.requires_shipping,
fulfillmentStatus: item.fulfillment_status
})),
// Shipping info
shipping: {
method: order.shipping_lines[0]?.title,
address: order.shipping_address ? {
name: `${order.shipping_address.first_name} ${order.shipping_address.last_name}`,
company: order.shipping_address.company,
address1: order.shipping_address.address1,
address2: order.shipping_address.address2,
city: order.shipping_address.city,
province: order.shipping_address.province,
country: order.shipping_address.country,
zip: order.shipping_address.zip,
phone: order.shipping_address.phone
} : null
},
// Flags
flags: {
isFirstOrder: (order.customer?.orders_count || 1) === 1,
hasDiscount: parseFloat(order.total_discounts) > 0,
isPaid: order.financial_status === 'paid',
requiresShipping: order.line_items.some(item => item.requires_shipping),
hasFraudRisk: order.fraud_analysis?.risk_level === 'high'
},
// Discount codes
discountCodes: order.discount_codes || [],
// Tags and notes
tags: order.tags ? order.tags.split(',').map(t => t.trim()) : [],
note: order.note,
noteAttributes: order.note_attributes
};
return [{ json: orderData }];Order Routing Logic
// Function Node: Route Order by Criteria
const order = $input.first().json;
// Determine fulfillment route
let route = 'standard';
let priority = 'normal';
let warehouse = 'main';
// High value orders
if (order.financials.totalPrice > 500) {
priority = 'high';
}
// First-time customer
if (order.flags.isFirstOrder) {
order.flags.requiresWelcomeEmail = true;
}
// Fraud risk
if (order.flags.hasFraudRisk) {
route = 'manual_review';
priority = 'hold';
}
// Express shipping
const shippingMethod = order.shipping.method?.toLowerCase() || '';
if (shippingMethod.includes('express') || shippingMethod.includes('overnight')) {
priority = 'urgent';
}
// International orders
if (order.shipping.address?.country !== 'United States') {
route = 'international';
warehouse = 'international';
}
// Specific product routing
const requiresSpecialHandling = order.items.some(item =>
item.tags?.includes('fragile') ||
item.tags?.includes('hazmat') ||
item.sku?.startsWith('SPECIAL-')
);
if (requiresSpecialHandling) {
route = 'special_handling';
}
return [{
json: {
...order,
routing: {
route,
priority,
warehouse,
assignedAt: new Date().toISOString()
}
}
}];Inventory Management
Real-Time Inventory Sync
// Function Node: Inventory Level Handler
const inventoryUpdate = $input.first().json;
// Process inventory level change
const inventoryData = {
inventoryItemId: inventoryUpdate.inventory_item_id,
locationId: inventoryUpdate.location_id,
available: inventoryUpdate.available,
updatedAt: inventoryUpdate.updated_at
};
// Check for low stock threshold
const lowStockThreshold = 10;
const criticalStockThreshold = 3;
let stockStatus = 'normal';
let alertRequired = false;
if (inventoryData.available <= 0) {
stockStatus = 'out_of_stock';
alertRequired = true;
} else if (inventoryData.available <= criticalStockThreshold) {
stockStatus = 'critical';
alertRequired = true;
} else if (inventoryData.available <= lowStockThreshold) {
stockStatus = 'low';
alertRequired = true;
}
return [{
json: {
...inventoryData,
stockStatus,
alertRequired,
thresholds: {
low: lowStockThreshold,
critical: criticalStockThreshold
}
}
}];Multi-Location Inventory Aggregation
// Function Node: Aggregate Inventory Across Locations
const inventoryLevels = $input.all().map(item => item.json);
// Group by inventory item
const aggregated = {};
for (const level of inventoryLevels) {
const itemId = level.inventory_item_id;
if (!aggregated[itemId]) {
aggregated[itemId] = {
inventoryItemId: itemId,
totalAvailable: 0,
totalOnHand: 0,
locations: [],
lastUpdated: level.updated_at
};
}
aggregated[itemId].totalAvailable += level.available || 0;
aggregated[itemId].totalOnHand += level.on_hand || 0;
aggregated[itemId].locations.push({
locationId: level.location_id,
locationName: level.location_name,
available: level.available,
onHand: level.on_hand,
incoming: level.incoming || 0
});
// Track most recent update
if (level.updated_at > aggregated[itemId].lastUpdated) {
aggregated[itemId].lastUpdated = level.updated_at;
}
}
// Convert to array and calculate status
const results = Object.values(aggregated).map(item => ({
...item,
status: item.totalAvailable <= 0 ? 'out_of_stock' :
item.totalAvailable <= 10 ? 'low_stock' : 'in_stock',
needsReorder: item.totalAvailable <= 20
}));
return results.map(item => ({ json: item }));Automated Reorder Workflow
// Function Node: Generate Purchase Order
const lowStockItems = $input.all().map(item => item.json);
// Group items by supplier
const bySupplier = {};
for (const item of lowStockItems) {
const supplier = item.supplier || 'default';
if (!bySupplier[supplier]) {
bySupplier[supplier] = {
supplier,
items: [],
totalItems: 0,
estimatedCost: 0
};
}
// Calculate reorder quantity
const reorderPoint = item.reorderPoint || 20;
const reorderQuantity = item.reorderQuantity || 50;
const currentStock = item.totalAvailable;
const quantityToOrder = Math.max(0, reorderQuantity - currentStock + reorderPoint);
if (quantityToOrder > 0) {
bySupplier[supplier].items.push({
sku: item.sku,
productTitle: item.productTitle,
currentStock,
reorderQuantity: quantityToOrder,
unitCost: item.unitCost || 0,
totalCost: quantityToOrder * (item.unitCost || 0)
});
bySupplier[supplier].totalItems += quantityToOrder;
bySupplier[supplier].estimatedCost += quantityToOrder * (item.unitCost || 0);
}
}
// Generate purchase orders
const purchaseOrders = Object.values(bySupplier)
.filter(po => po.items.length > 0)
.map((po, index) => ({
...po,
poNumber: `PO-${Date.now()}-${index + 1}`,
createdAt: new Date().toISOString(),
status: 'draft',
expectedDelivery: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
}));
return purchaseOrders.map(po => ({ json: po }));Customer Communication Automation
Order Confirmation Email
// Function Node: Generate Order Confirmation Email
const order = $input.first().json;
// Format line items for email
const itemsHtml = order.items.map(item => `
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;">
${item.title}${item.variantTitle ? ` - ${item.variantTitle}` : ''}
</td>
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;">
${item.quantity}
</td>
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: right;">
$${(item.price * item.quantity).toFixed(2)}
</td>
</tr>
`).join('');
// Format shipping address
const address = order.shipping.address;
const addressHtml = address ? `
<p>
${address.name}<br>
${address.company ? address.company + '<br>' : ''}
${address.address1}<br>
${address.address2 ? address.address2 + '<br>' : ''}
${address.city}, ${address.province} ${address.zip}<br>
${address.country}
</p>
` : '<p>Digital delivery - no shipping required</p>';
const emailHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }
.header { background: #2563eb; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
table { width: 100%; border-collapse: collapse; }
th { background: #f3f4f6; padding: 10px; text-align: left; }
.totals { margin-top: 20px; text-align: right; }
.footer { background: #f9fafb; padding: 20px; text-align: center; font-size: 12px; }
</style>
</head>
<body>
<div class="header">
<h1>Order Confirmed!</h1>
<p>Order ${order.orderName}</p>
</div>
<div class="content">
<p>Hi ${order.customer.firstName || 'there'},</p>
<p>Thank you for your order! We've received it and are getting it ready.</p>
<h3>Order Details</h3>
<table>
<thead>
<tr>
<th>Item</th>
<th style="text-align: center;">Qty</th>
<th style="text-align: right;">Price</th>
</tr>
</thead>
<tbody>
${itemsHtml}
</tbody>
</table>
<div class="totals">
<p>Subtotal: $${order.financials.subtotal.toFixed(2)}</p>
${order.financials.totalDiscount > 0 ? `<p>Discount: -$${order.financials.totalDiscount.toFixed(2)}</p>` : ''}
<p>Shipping: $${order.financials.totalShipping.toFixed(2)}</p>
<p>Tax: $${order.financials.totalTax.toFixed(2)}</p>
<p><strong>Total: $${order.financials.totalPrice.toFixed(2)} ${order.financials.currency}</strong></p>
</div>
<h3>Shipping Address</h3>
${addressHtml}
<p>You'll receive another email when your order ships with tracking information.</p>
</div>
<div class="footer">
<p>Questions? Reply to this email or contact us at support@example.com</p>
</div>
</body>
</html>
`;
return [{
json: {
to: order.customer.email,
subject: `Order Confirmed - ${order.orderName}`,
html: emailHtml,
tags: ['order-confirmation', `order-${order.orderId}`]
}
}];Shipping Notification
// Function Node: Generate Shipping Notification
const fulfillment = $input.first().json;
const trackingUrl = fulfillment.tracking_url ||
(fulfillment.tracking_company === 'USPS' ?
`https://tools.usps.com/go/TrackConfirmAction?tLabels=${fulfillment.tracking_number}` :
fulfillment.tracking_company === 'UPS' ?
`https://www.ups.com/track?tracknum=${fulfillment.tracking_number}` :
fulfillment.tracking_company === 'FedEx' ?
`https://www.fedex.com/fedextrack/?trknbr=${fulfillment.tracking_number}` :
null);
const emailHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }
.header { background: #059669; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
.tracking-box { background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0; text-align: center; }
.track-button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 10px; }
.footer { background: #f9fafb; padding: 20px; text-align: center; font-size: 12px; }
</style>
</head>
<body>
<div class="header">
<h1>📦 Your Order Has Shipped!</h1>
</div>
<div class="content">
<p>Great news! Your order is on its way.</p>
<div class="tracking-box">
<p><strong>Carrier:</strong> ${fulfillment.tracking_company || 'Standard Shipping'}</p>
${fulfillment.tracking_number ? `<p><strong>Tracking Number:</strong> ${fulfillment.tracking_number}</p>` : ''}
${trackingUrl ? `<a href="${trackingUrl}" class="track-button">Track Your Package</a>` : ''}
</div>
<h3>Items Shipped</h3>
<ul>
${fulfillment.line_items.map(item =>
`<li>${item.title}${item.variant_title ? ` - ${item.variant_title}` : ''} (x${item.quantity})</li>`
).join('')}
</ul>
<p>Estimated delivery: ${fulfillment.estimated_delivery_at ?
new Date(fulfillment.estimated_delivery_at).toLocaleDateString() :
'3-5 business days'}</p>
</div>
<div class="footer">
<p>Questions about your shipment? Contact us at support@example.com</p>
</div>
</body>
</html>
`;
return [{
json: {
to: fulfillment.customer_email,
subject: 'Your Order Has Shipped! 📦',
html: emailHtml,
tags: ['shipping-notification', `order-${fulfillment.order_id}`]
}
}];Fulfillment Automation
Fulfillment Service Integration
// Function Node: Create Fulfillment Request
const order = $input.first().json;
// Prepare fulfillment request for shipping carrier
const fulfillmentRequest = {
orderId: order.orderId,
orderNumber: order.orderNumber,
shipTo: {
name: order.shipping.address.name,
company: order.shipping.address.company,
street1: order.shipping.address.address1,
street2: order.shipping.address.address2,
city: order.shipping.address.city,
state: order.shipping.address.province,
postalCode: order.shipping.address.zip,
country: order.shipping.address.country,
phone: order.shipping.address.phone
},
shipFrom: {
name: 'Your Store',
street1: '123 Warehouse St',
city: 'Commerce City',
state: 'CA',
postalCode: '90210',
country: 'United States',
phone: '555-123-4567'
},
packages: [{
weight: order.items.reduce((sum, item) =>
sum + (item.weight || 0.5) * item.quantity, 0
),
weightUnit: 'lb',
dimensions: {
length: 10,
width: 8,
height: 4,
unit: 'in'
}
}],
items: order.items.map(item => ({
sku: item.sku,
description: item.title,
quantity: item.quantity,
value: item.price,
weight: item.weight || 0.5
})),
serviceType: mapShippingService(order.shipping.method),
shipDate: new Date().toISOString().split('T')[0],
reference: order.orderNumber,
options: {
signatureRequired: order.financials.totalPrice > 100,
insurance: order.financials.totalPrice > 200 ? {
amount: order.financials.totalPrice,
currency: order.financials.currency
} : null
}
};
function mapShippingService(shopifyMethod) {
const mappings = {
'Standard Shipping': 'ground',
'Express Shipping': 'express',
'Overnight': 'overnight',
'Free Shipping': 'ground'
};
return mappings[shopifyMethod] || 'ground';
}
return [{ json: fulfillmentRequest }];Update Shopify Fulfillment
// Function Node: Update Shopify with Fulfillment
const carrierResponse = $input.first().json;
const originalOrder = $input.first().json.orderData;
// Build Shopify fulfillment payload
const fulfillmentPayload = {
fulfillment: {
location_id: $env.SHOPIFY_LOCATION_ID,
tracking_number: carrierResponse.trackingNumber,
tracking_company: carrierResponse.carrier,
tracking_url: carrierResponse.trackingUrl,
notify_customer: true,
line_items: originalOrder.items.map(item => ({
id: item.id,
quantity: item.quantity
}))
}
};
return [{
json: {
endpoint: `/admin/api/2024-01/orders/${originalOrder.orderId}/fulfillments.json`,
method: 'POST',
payload: fulfillmentPayload
}
}];Analytics and Reporting
Daily Sales Summary
// Function Node: Generate Daily Sales Report
const orders = $input.all().map(item => item.json);
// Calculate metrics
const metrics = {
date: new Date().toISOString().split('T')[0],
totalOrders: orders.length,
totalRevenue: 0,
averageOrderValue: 0,
totalItems: 0,
newCustomers: 0,
returningCustomers: 0,
topProducts: {},
revenueByHour: new Array(24).fill(0),
ordersByCountry: {}
};
for (const order of orders) {
// Revenue
metrics.totalRevenue += order.financials.totalPrice;
// Items
metrics.totalItems += order.items.reduce((sum, item) => sum + item.quantity, 0);
// Customer type
if (order.flags.isFirstOrder) {
metrics.newCustomers++;
} else {
metrics.returningCustomers++;
}
// Product popularity
for (const item of order.items) {
const key = item.sku || item.title;
if (!metrics.topProducts[key]) {
metrics.topProducts[key] = { title: item.title, quantity: 0, revenue: 0 };
}
metrics.topProducts[key].quantity += item.quantity;
metrics.topProducts[key].revenue += item.price * item.quantity;
}
// Revenue by hour
const hour = new Date(order.createdAt).getHours();
metrics.revenueByHour[hour] += order.financials.totalPrice;
// Geography
const country = order.shipping.address?.country || 'Unknown';
metrics.ordersByCountry[country] = (metrics.ordersByCountry[country] || 0) + 1;
}
// Calculate averages
metrics.averageOrderValue = metrics.totalOrders > 0 ?
metrics.totalRevenue / metrics.totalOrders : 0;
// Sort top products
metrics.topProducts = Object.entries(metrics.topProducts)
.sort((a, b) => b[1].revenue - a[1].revenue)
.slice(0, 10)
.map(([sku, data]) => ({ sku, ...data }));
return [{ json: metrics }];Error Handling and Monitoring
Workflow Error Handler
// Function Node: E-commerce Error Handler
const error = $input.first().json.error;
const context = $input.first().json.context;
// Categorize error
let category = 'unknown';
let severity = 'medium';
let action = 'log';
const errorMessage = error.message?.toLowerCase() || '';
if (errorMessage.includes('rate limit') || errorMessage.includes('429')) {
category = 'rate_limit';
severity = 'low';
action = 'retry_with_backoff';
} else if (errorMessage.includes('not found') || errorMessage.includes('404')) {
category = 'not_found';
severity = 'medium';
action = 'skip';
} else if (errorMessage.includes('unauthorized') || errorMessage.includes('401')) {
category = 'auth';
severity = 'high';
action = 'alert';
} else if (errorMessage.includes('inventory') || errorMessage.includes('stock')) {
category = 'inventory';
severity = 'high';
action = 'manual_review';
} else if (errorMessage.includes('payment')) {
category = 'payment';
severity = 'critical';
action = 'alert_urgent';
}
const errorReport = {
timestamp: new Date().toISOString(),
category,
severity,
action,
error: {
message: error.message,
code: error.code,
stack: error.stack?.slice(0, 500)
},
context: {
orderId: context?.orderId,
operation: context?.operation,
workflowId: $workflow.id,
executionId: $execution.id
}
};
return [{
json: {
...errorReport,
shouldRetry: action.includes('retry'),
shouldAlert: action.includes('alert'),
alertChannel: severity === 'critical' ? 'pagerduty' : 'slack'
}
}];Best Practices Summary
- Validate webhooks using HMAC signatures
- Handle rate limits with exponential backoff
- Use idempotency keys to prevent duplicate processing
- Monitor inventory in real-time across locations
- Personalize communications based on customer history
- Implement error handling with appropriate escalation
- Track analytics for business insights
- Test thoroughly before going live
E-commerce automation with n8n enables scaling operations while maintaining customer experience quality. Start with core order processing and expand to inventory and marketing automation as you grow.