Inventory synchronization between Shopify and your ERP doesn't require expensive iPaaS platforms or custom-coded integrations. n8n, an open-source workflow automation tool, gives you the flexibility to build bidirectional inventory sync tailored to your exact business needs.
This guide shows you how to configure n8n for real-time inventory synchronization between Shopify and ERP systems like Business Central, Acumatica, NetSuite, or custom databases.
n8n is an open-source workflow automation platform with a visual editor for building integrations.
Key advantages:
For manufacturers managing thousands of SKUs across multiple locations, n8n handles complex inventory scenarios without vendor lock-in.
Before building n8n workflows, you need:
1. n8n instance
2. Shopify API access
3. ERP API access
4. Understanding of your inventory data model
For more on Shopify ERP integration fundamentals, see our guide on Shopify ERP Integration.
Create a test workflow:
If you see product data, the connection works.
This workflow updates Shopify inventory when your ERP stock levels change.
1. Webhook Trigger (or Schedule Trigger)
Option A: Real-time sync (webhook)
Option B: Scheduled sync (polling)
2. HTTP Request (Query ERP for inventory data)
If using scheduled sync:
Response format should include:
[
{
"sku": "WIDGET-001",
"location": "MAIN-WAREHOUSE",
"available_quantity": 150
},
{
"sku": "WIDGET-002",
"location": "MAIN-WAREHOUSE",
"available_quantity": 75
}
]
3. Split in Batches (Process inventory updates in batches)
Shopify API has rate limits. Process inventory updates in batches of 50-100 items.
4. Code Node (Map ERP SKUs to Shopify inventory item IDs)
You need to convert ERP SKUs to Shopify's inventory item IDs. Store this mapping in a database or fetch it dynamically.
// Get inventory items from previous node
const erpItems = $input.all();
// Map to Shopify format
const shopifyUpdates = erpItems.map(item => {
return {
inventoryItemId: item.json.shopify_inventory_item_id, // You need to maintain this mapping
locationId: item.json.shopify_location_id,
availableQuantity: item.json.available_quantity
};
});
return shopifyUpdates;
5. Shopify GraphQL Node (Update inventory levels)
Use GraphQL for inventory updates:
mutation inventorySetOnHandQuantities($input: InventorySetOnHandQuantitiesInput!) {
inventorySetOnHandQuantities(input: $input) {
userErrors {
field
message
}
inventoryAdjustmentGroup {
createdAt
reason
changes {
name
delta
}
}
}
}
Variables:
{
"input": {
"reason": "correction",
"setQuantities": [
{
"inventoryItemId": "gid://shopify/InventoryItem/123456789",
"locationId": "gid://shopify/Location/987654321",
"quantity": 150
}
]
}
}
6. IF Node (Check for errors)
Check if Shopify returned errors:
7. Error logging
On error branch, log to database or send notification:
[Webhook/Schedule] → [HTTP Request (ERP)] → [Split in Batches] → [Code (Map SKUs)] → [Shopify GraphQL] → [IF (Check Errors)] → [Success/Error Handling]
When orders are placed on Shopify, update ERP inventory to reflect reserved or sold stock.
1. Shopify Trigger (Order Created webhook)
2. Code Node (Extract order line items)
const order = $input.first().json;
// Extract line items
const lineItems = order.line_items.map(item => {
return {
sku: item.sku,
quantity: item.quantity,
variant_id: item.variant_id,
order_number: order.order_number
};
});
return lineItems;
3. Loop Over Items
Use Split in Batches to process each line item.
4. HTTP Request (Update ERP inventory)
For each line item, call your ERP API to reserve or reduce inventory:
POST https://your-erp.com/api/inventory/reduce
Content-Type: application/json
Authorization: Bearer YOUR_ERP_TOKEN
{
"sku": "WIDGET-001",
"quantity": 2,
"reason": "shopify_order",
"reference": "SH-1234"
}
5. Error handling
If ERP update fails:
You have three warehouses in your ERP (EAST, WEST, CENTRAL) mapped to three Shopify locations.
Workflow modification:
In the Code node, map each ERP location to its corresponding Shopify location ID:
const locationMapping = { 'EAST': 'gid://shopify/Location/111111111', 'WEST': 'gid://shopify/Location/222222222', 'CENTRAL': 'gid://shopify/Location/333333333'};const erpItems = $input.all();const shopifyUpdates = erpItems.map(item => { return { inventoryItemId: item.json.shopify_inventory_item_id, locationId: locationMapping[item.json.erp_location], availableQuantity: item.json.available_quantity };});return shopifyUpdates;Your ERP tracks total on-hand quantity, but you want to reserve 10% as safety stock and only sync 90% to Shopify.
Workflow modification:
const safetyStockPercentage = 0.10;
const shopifyUpdates = erpItems.map(item => {
const onHandQty = item.json.on_hand_quantity;
const safetyStock = Math.ceil(onHandQty * safetyStockPercentage);
const availableQty = Math.max(0, onHandQty - safetyStock);
return {
inventoryItemId: item.json.shopify_inventory_item_id,
locationId: item.json.shopify_location_id,
availableQuantity: availableQty
};
});
return shopifyUpdates;If your ERP allows negative inventory (backorders), but Shopify doesn't, cap the quantity at zero.
Workflow modification:
const shopifyUpdates = erpItems.map(item => {
const erpQty = item.json.available_quantity;
const shopifyQty = Math.max(0, erpQty); // Never send negative quantities
return {
inventoryItemId: item.json.shopify_inventory_item_id,
locationId: item.json.shopify_location_id,
availableQuantity: shopifyQty
};
});
return shopifyUpdates;For temporary inventory holds or manual adjustments, sync from Google Sheets instead of ERP.
Workflow:
Use case: Sales team manually reserves inventory for a pending quote. They update a Google Sheet with SKU and reserved quantity. n8n syncs this to Shopify to prevent overselling.
For the complete workflow template, see n8n's Shopify bulk product creation workflow.
The biggest challenge in inventory sync is maintaining accurate mapping between ERP and Shopify identifiers.
Create a mapping table in PostgreSQL, MySQL, or similar:
CREATE TABLE inventory_mapping (
id SERIAL PRIMARY KEY,
erp_sku VARCHAR(100) NOT NULL,
erp_location VARCHAR(100) NOT NULL,
shopify_inventory_item_id VARCHAR(100) NOT NULL,
shopify_location_id VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Workflow modification:
Before updating Shopify, query this table to get Shopify IDs:
// In HTTP Request node
const erpSku = $json.sku;
const erpLocation = $json.location;
// Query your database
const query = `
SELECT shopify_inventory_item_id, shopify_location_id
FROM inventory_mapping
WHERE erp_sku = '${erpSku}' AND erp_location = '${erpLocation}'
`;
// Use PostgreSQL node to execute query
// Then merge results with inventory data
Store ERP SKU and location in Shopify product metafields. When syncing, query Shopify products by metafield to find the matching inventory item.
Metafield structure:
GraphQL query to find product by ERP SKU:
query {
products(first: 1, query: "metafield.erp.sku:WIDGET-001") {
edges {
node {
id
variants(first: 1) {
edges {
node {
inventoryItem {
id
}
}
}
}
}
}
}
}
This is slower than a database mapping table but requires less infrastructure.
For catalogs under 1,000 SKUs, load all mappings into memory at workflow start:
// In Code node at start of workflow
const axios = require('axios');
// Fetch all Shopify products
const response = await axios.get('https://your-store.myshopify.com/admin/api/2025-04/products.json', {
headers: { 'X-Shopify-Access-Token': 'YOUR_TOKEN' }
});
// Build mapping object
const mapping = {};
response.data.products.forEach(product => {
product.variants.forEach(variant => {
mapping[variant.sku] = {
inventoryItemId: variant.inventory_item_id,
variantId: variant.id
};
});
});
// Store in workflow context
$node["Workflow"].json.mapping = mapping;
Later nodes can reference this mapping without additional API calls.
Inventory sync must be reliable. Implement these error handling patterns:
If Shopify API returns rate limit error (429), wait and retry:
const maxRetries = 3;
let retryCount = 0;
let success = false;
while (retryCount < maxRetries && !success) {
try {
// Attempt Shopify API call
const result = await shopifyApiCall();
success = true;
return result;
} catch (error) {
if (error.statusCode === 429) {
// Rate limited, wait and retry
const waitTime = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, waitTime));
retryCount++;
} else {
throw error; // Different error, don't retry
}
}
}
throw new Error('Max retries exceeded');If an inventory update fails after retries, log it to a "failed updates" table for manual review:
// In error handling branch
const failedUpdate = {
sku: $json.sku,
attempted_quantity: $json.quantity,
error_message: $json.error,
timestamp: new Date().toISOString(),
retry_count: $json.retryCount || 0
};
// Insert into database
// Send alert to operations teamRun a nightly reconciliation workflow that compares ERP inventory to Shopify inventory and flags discrepancies:
// Fetch all inventory from ERP
const erpInventory = await fetchERPInventory();
// Fetch all inventory from Shopify
const shopifyInventory = await fetchShopifyInventory();
// Compare
const discrepancies = [];
erpInventory.forEach(erpItem => {
const shopifyItem = shopifyInventory.find(s => s.sku === erpItem.sku);
if (!shopifyItem) {
discrepancies.push({ sku: erpItem.sku, issue: 'Missing in Shopify' });
} else if (erpItem.quantity !== shopifyItem.quantity) {
discrepancies.push({
sku: erpItem.sku,
issue: 'Quantity mismatch',
erp_qty: erpItem.quantity,
shopify_qty: shopifyItem.quantity
});
}
});
// Send report
return discrepancies;
Workflow execution tracking: Use n8n's execution history to track daily executions, success rate, and error types.
Slack alerts: Add Slack node to error branches with SKU, error message, and timestamp.
Daily summary report: Schedule workflow to send daily sync statistics (total updates, success rate, common errors).
Discrepancy alerts: Send immediate notifications when reconciliation finds quantity mismatches.
For large catalogs (10,000+ SKUs):
Use n8n when:
Use native connectors when:
See Business Central Connector, Acumatica Connector, or n8n vs Shopify Flow.
n8n provides flexible, cost-effective inventory synchronization between Shopify and any ERP with an API. Unlike proprietary platforms, n8n offers self-hosting, unlimited workflows, and full customization.
Success requires accurate SKU and location mapping between systems. Use database tables for large catalogs or product metafields for smaller ones.
Implement error handling with retries, dead letter queues, and reconciliation workflows. Monitor performance and alert on failures.
For 10,000+ SKUs, optimize with batching, GraphQL, caching, and parallel processing.
n8n works best for custom logic, multi-system integration, or unsupported ERPs. Native connectors may be simpler for standard use cases.
Related resources: