Building Bulletproof Shopify-to-ERP Automations with n8n and 8kit
8kit Team•
Shopify-to-ERP sync is one of the most common, and most fragile, automations people build with n8n. Orders flow from Shopify, get transformed, and land in your ERP (Xero, QuickBooks, NetSuite, SAP Business One, or a custom system). When it works, it's magical. When it breaks, invoices go missing, inventory drifts, and someone spends Friday night reconciling spreadsheets.
This guide walks through building a production-grade Shopify-to-ERP sync with n8n that handles every edge case: duplicate webhooks, concurrent order updates, ID mapping across systems, and incremental sync for bulk operations. We'll use 8kit's four patterns to make it bulletproof.
The Typical Setup (and Where It Breaks)
Most Shopify-to-ERP n8n workflows start like this:
Shopify Webhook (orders/create)
→ Transform order data
→ Create Invoice in ERP
→ Create/Update Customer in ERP
→ Update Inventory in ERP
Simple. Clean. And it will fail in production. Here's how:
Failure 1: Shopify sends the webhook twice
Shopify's webhook infrastructure does not guarantee exactly-once delivery. If your endpoint is slow to respond (n8n is processing the order), Shopify considers the delivery failed and retries. Your ERP now has two invoices for the same order.
How often: Shopify retries up to 19 times over 48 hours for failed deliveries. Even "successful" deliveries can duplicate if there's a network hiccup between Shopify and your n8n instance.
Failure 2: Customer places two orders simultaneously
Two orders/create webhooks fire within milliseconds. Both workflows look up the customer in the ERP. Neither finds them (customer is new). Both create the customer. Now you have duplicate customer records, and two invoices pointing to different customer IDs.
Failure 3: Finding the ERP customer
For returning customers, your workflow needs to find the matching customer record in the ERP. Search by email? What if they used a different email. Search by name? "John Smith" matches 47 records. Without a reliable ID mapping, you're either guessing or failing.
Failure 4: The nightly sync re-processes everything
You also have a scheduled workflow that syncs all orders from the last 24 hours as a safety net. But it re-processes every order, including the ones already handled by webhooks. Duplicate invoices again.
The Bulletproof Architecture
Here's the same sync rebuilt with 8kit:
Workflow 1: Real-time order processing (webhook)
Shopify Webhook (orders/create or orders/updated)
│
├─ 8kit Uniqs: Check "shopify-orders" / {{ $json.id }}-{{ $json.updated_at }}
│ └─ If already seen → Exit (skip duplicate webhook)
│
├─ 8kit Exclusivity: Acquire lock "order:{{ $json.id }}"
│ └─ Prevents concurrent processing of same order
│
├─ 8kit Lookup: Get "shopify-erp-customers" / {{ $json.customer.id }}
│ ├─ If mapping exists → Use ERP customer ID
│ └─ If no mapping:
│ ├─ Create customer in ERP
│ └─ 8kit Lookup: Set "shopify-erp-customers" mapping
│
├─ 8kit Lookup: Get "shopify-erp-orders" / {{ $json.id }}
│ ├─ If mapping exists → Update existing ERP invoice
│ └─ If no mapping:
│ ├─ Create invoice in ERP
│ └─ 8kit Lookup: Set "shopify-erp-orders" mapping
│
├─ Update inventory in ERP (using mapped product IDs)
│
├─ 8kit Exclusivity: Release lock "order:{{ $json.id }}"
│
└─ 8kit Uniqs: Mark "shopify-orders" / {{ $json.id }}-{{ $json.updated_at }} as done
Why this works:
- Uniqs dedup key includes
updated_at, so order updates get processed but exact retries are skipped - Exclusivity ensures two webhooks for the same order can't race
- Lookups provide instant, reliable ID resolution, no searching by email
- Processing is marked done only after everything succeeds
Workflow 2: Scheduled catch-up sync
Schedule Trigger (every 6 hours)
│
├─ 8kit Temporal: Get "shopify-erp-sync" timestamp
│ └─ Returns last successful sync time
│
├─ Shopify: Get orders updated since {{ timestamp }}
│
├─ Split Into Batches (50 at a time)
│ │
│ ├─ 8kit Uniqs: Check "shopify-orders" / {{ $json.id }}-{{ $json.updated_at }}
│ │ └─ Skip orders already processed by webhook workflow
│ │
│ └─ [Same processing logic as Workflow 1]
│
└─ 8kit Temporal: Set "shopify-erp-sync" to now
Why this works:
- Temporal fetches only orders changed since last sync (not all orders)
- Uniqs skips orders already handled by the webhook workflow (shared namespace)
- The catch-up only processes orders that fell through the cracks
Shared patterns across both workflows
The key insight: both workflows share the same 8kit namespaces:
shopify-orders(Uniqs), same dedup pool regardless of trigger sourceshopify-erp-customers(Lookups), same customer mappingshopify-erp-orders(Lookups), same order mapping
This means the webhook and the scheduled sync cooperate automatically. No coordination logic needed.
Building It Step by Step
Step 1: Set up ID mappings
Before processing orders, establish your mapping namespaces:
Customer mapping: shopify-erp-customers
- Key: Shopify customer ID
- Value: ERP customer ID (or object with ID + metadata)
Order mapping: shopify-erp-orders
- Key: Shopify order ID
- Value: ERP invoice/order ID
Product mapping: shopify-erp-products
- Key: Shopify product variant ID
- Value: ERP item/SKU ID
For product mappings, you may want to pre-populate these from a manual CSV import or a one-time sync workflow. Customer and order mappings build organically as orders flow through.
Step 2: Handle order lifecycle events
Shopify orders go through multiple states. Your workflow needs to handle:
| Shopify Event | n8n Action |
|---|---|
orders/create | Create invoice + customer (if new) in ERP |
orders/updated | Update existing invoice in ERP |
orders/paid | Mark invoice as paid in ERP |
orders/cancelled | Void or credit the invoice in ERP |
orders/fulfilled | Update fulfillment status in ERP |
refunds/create | Create credit note in ERP |
Each event uses the same Lookup mapping to find the ERP record. The Uniqs dedup key should include the event type and updated timestamp to handle each event exactly once:
Dedup key: {{ $json.id }}-{{ $json.topic }}-{{ $json.updated_at }}
Step 3: Error handling and recovery
When an ERP API call fails:
- Don't mark as processed, the Uniqs node at the end of the workflow doesn't execute, so the next run will retry
- Release the lock, use n8n's Error Trigger to ensure the Exclusivity lock is released on failure (or rely on 8kit's TTL auto-release)
- Log the failure, send to your error tracking (Sentry, Slack, etc.)
Error Trigger
→ 8kit Exclusivity: Release lock (if held)
→ Send Slack notification with order details
Step 4: Initial data load
For first-time setup with existing Shopify data:
Manual Trigger
→ Shopify: Get ALL orders (paginated)
→ Split Into Batches (50)
→ [Same processing logic]
→ 8kit Temporal: Set initial sync timestamp
This populates your Lookup mappings and sets the Temporal baseline. All subsequent runs are incremental.
Monitoring Your Sync
Use the 8kit dashboard to monitor:
- Active locks, if a lock is held for too long, an execution may be stuck
- Lookup count, growing mapping count confirms orders are flowing
- Uniqs count, duplicate rejection rate shows webhook reliability
- Temporal timestamps, gap between "now" and last sync indicates processing lag
The Numbers
For a mid-volume Shopify store (500 orders/day):
| Without 8kit | With 8kit |
|---|---|
| ~50 duplicate invoices/month | 0 duplicate invoices |
| 2-3 race condition incidents/week | 0 race conditions |
| Full order re-scan every sync | Only changed orders |
| Manual customer dedup in ERP monthly | Automatic, from day 1 |
| 4+ hours/month manual reconciliation | 0 hours |
Getting Started
- Install 8kit in your n8n instance
- Map your existing workflows to the architecture above
- Start with Workflow 1 (webhook processing with dedup + locking)
- Add Workflow 2 (scheduled catch-up) once webhook processing is stable
- Monitor via the 8kit dashboard
The goal: zero duplicate records, zero race conditions, zero full-table scans. That's what bulletproof looks like.
8kit provides four enterprise automation patterns for n8n: deduplication, cross-system mapping, distributed locking, and change tracking. Learn more at 8kit.io