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.

What is n8n and Why Use It for Inventory Sync?

n8n is an open-source workflow automation platform with a visual editor for building integrations.

Key advantages:

  • Self-hosted option: Deploy on your infrastructure for data control and no per-task pricing
  • Custom logic: JavaScript and Python support for complex inventory calculations
  • Native Shopify nodes: Pre-built webhooks, GraphQL, and REST API nodes
  • Universal ERP support: HTTP requests and database queries connect to any system
  • Cost-effective: Free self-hosted or affordable cloud pricing

For manufacturers managing thousands of SKUs across multiple locations, n8n handles complex inventory scenarios without vendor lock-in.

Prerequisites for Inventory Sync

Before building n8n workflows, you need:

1. n8n instance

  • Self-hosted (Docker, npm, or cloud VPS)
  • n8n Cloud account (managed hosting)

2. Shopify API access

  • Custom app in Shopify Admin with inventory permissions
  • Admin API access token
  • Webhook configuration (for real-time sync)

3. ERP API access

  • REST API or SOAP endpoint
  • Authentication credentials (API key, OAuth, basic auth)
  • Inventory endpoint documentation

4. Understanding of your inventory data model

  • SKU mapping between Shopify and ERP
  • Location mapping (warehouses, stores, fulfillment centers)
  • Stock calculation logic (available vs. reserved vs. on-hand)

For more on Shopify ERP integration fundamentals, see our guide on Shopify ERP Integration.

Setting Up Shopify Connection in n8n

Step 1: Create a custom app in Shopify

  1. Go to Settings > Apps and sales channels > Develop apps
  2. Click Create an app
  3. Name it (e.g., "n8n Inventory Sync")
  4. Click Configure Admin API scopes
  5. Select these scopes:
    1. read_products (read product data)
    2. write_products (update product data)
    3. read_inventory (read inventory levels)
    4. write_inventory (update inventory levels)
    5. read_locations (read location data)
  6. Click Save
  7. Click Install app
  8. Reveal and copy the Admin API access token

Step 2: Configure n8n Shopify credentials

  1. In n8n, go to Credentials
  2. Click Add Credential
  3. Select Shopify API
  4. Enter:
    1. Shop Subdomain: your-store (without .myshopify.com)
    2. Access Token: Paste the Admin API token
    3. API Version: 2025-04 (or latest stable version)
  5. Click Save

Step 3: Test the connection

Create a test workflow:

  • Add Shopify node
  • Set Resource: Product
  • Set Operation: Get All
  • Set Limit: 5
  • Execute the node

If you see product data, the connection works.

Building a Basic Inventory Sync Workflow (ERP to Shopify)

This workflow updates Shopify inventory when your ERP stock levels change.

Workflow nodes

1. Webhook Trigger (or Schedule Trigger)

Option A: Real-time sync (webhook)

  • Use Webhook node
  • Set Method: POST
  • Copy the webhook URL
  • Configure your ERP to send inventory updates to this URL when stock changes

Option B: Scheduled sync (polling)

  • Use Schedule Trigger node
  • Set interval (e.g., every 15 minutes)
  • Query ERP API for inventory updates

2. HTTP Request (Query ERP for inventory data)

If using scheduled sync:

  • Method: GET
  • URL: Your ERP inventory API endpoint (e.g., https://your-erp.com/api/inventory)
  • Authentication: Bearer Token, Basic Auth, or API Key
  • Headers: Set Content-Type, Authorization, etc.

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.

  • Batch Size: 50
  • Options: Reset

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:

  • Condition: {{ $json.userErrors.length > 0 }}
  • True branch: Log error, send alert
  • False branch: Continue to next item

7. Error logging

On error branch, log to database or send notification:

  • Slack node: Send error message to #inventory-alerts channel
  • Database node: Insert error log with SKU, error message, timestamp

Complete workflow visualization

[Webhook/Schedule] → [HTTP Request (ERP)] → [Split in Batches] → [Code (Map SKUs)] → [Shopify GraphQL] → [IF (Check Errors)] → [Success/Error Handling]

Building Reverse Sync (Shopify to ERP)

When orders are placed on Shopify, update ERP inventory to reflect reserved or sold stock.

Workflow nodes

1. Shopify Trigger (Order Created webhook)

  • Resource: Order
  • Event: Created
  • Webhook: Automatically configured by n8n

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:

  • Tag Shopify order with "erp-sync-failed"
  • Send alert to operations team
  • Log to database for manual reconciliation

Advanced Inventory Sync Scenarios

Scenario 1: Multi-location inventory sync

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;

Scenario 2: Stock calculation with safety stock

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;

Scenario 3: Handling backorders

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;

Scenario 4: Syncing inventory from Google Sheets (manual override)

For temporary inventory holds or manual adjustments, sync from Google Sheets instead of ERP.

Workflow:

  1. Google Sheets Trigger (when row is updated)
  2. Code Node (format data)
  3. Shopify GraphQL (update inventory)
  4. Google Sheets (mark row as synced)

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.

Maintaining SKU and Location Mapping

The biggest challenge in inventory sync is maintaining accurate mapping between ERP and Shopify identifiers.

Option 1: Database mapping table

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

Option 2: Shopify product metafields

Store ERP SKU and location in Shopify product metafields. When syncing, query Shopify products by metafield to find the matching inventory item.

Metafield structure:

  • Namespace: erp
  • Key: sku
  • Value: ERP SKU (e.g., "WIDGET-001")

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.

Option 3: In-memory mapping (for small catalogs)

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.

Error Handling and Retry Logic

Inventory sync must be reliable. Implement these error handling patterns:

Pattern 1: Exponential backoff retry

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');

Pattern 2: Dead letter queue

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 team

Pattern 3: Reconciliation workflow

Run 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;

Monitoring and Alerting

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.

Performance Optimization

For large catalogs (10,000+ SKUs):

  • Batch API calls: Send up to 100 inventory items per GraphQL mutation instead of individual updates.
  • Only sync changed items: Query ERP for items updated since last sync timestamp to reduce data transfer.
  • Use GraphQL over REST: More efficient for bulk operations (1 GraphQL mutation vs. 100 REST calls).
  • Parallel processing: Split updates across workflows by SKU range (A-M, N-Z) to double throughput.
  • Cache Shopify data: Store product and location data in memory for 1 hour to minimize API calls.

Best Practices for n8n Inventory Sync

  1. Use environment variables for credentials. Never hardcode API tokens. Use n8n's credential system.
  2. Version control workflows. Export as JSON and store in Git for rollback capability.
  3. Test in staging. Use Shopify development store before production deployment.
  4. Log all changes. Audit trail with old/new quantity, timestamp, and source.
  5. Set up failure alerts. Get notified immediately when sync fails.
  6. Run reconciliation regularly. Daily or weekly checks catch system drift early.
  7. Handle partial failures. Log errors and continue processing instead of failing entire batch.

When to Use n8n vs. Native Connectors

Use n8n when:

  • Your ERP lacks a native Shopify connector
  • You need custom inventory calculation logic
  • You're integrating multiple systems (ERP + WMS + 3PL)
  • You want full control over sync and error handling
  • You prefer self-hosted, cost-effective automation

Use native connectors when:

  • Your ERP has a robust native connector
  • Standard field mapping meets your needs
  • You prefer vendor support over self-management
  • You want automatic API compatibility updates

See Business Central Connector, Acumatica Connector, or n8n vs Shopify Flow.

What You Need to Know

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: