Ecommerce-to-ERP Integration: Order & Inventory Sync for Shopify, Magento, and BigCommerce
How do you integrate Shopify/Magento/BigCommerce with ERP for order and inventory sync?
TL;DR
- Bottom line: Use webhook-driven near-real-time sync for orders (ecommerce to ERP) and scheduled batch sync for inventory (ERP to ecommerce); never use bidirectional inventory sync without explicit conflict resolution. [src2, src6]
- Key limit: Shopify GraphQL Admin API allows 100 points/second (Standard) up to 1,000/s (Plus); BigCommerce allows 150-450 requests/30s by plan; Magento rate limiting is off by default. [src1, src4, src5]
- Watch out for: SKU mapping mismatches between platforms --
BLK-SHIRT-Lin Shopify vsBLACK_SHIRT_LARGEin your ERP will silently break sync. [src3, src7] - Best for: Retailers running 100-10,000 orders/day who need automated order flow into ERP with real-time inventory visibility back to the storefront. [src6]
- Authentication: Shopify uses OAuth 2.0 (custom apps) or access tokens (private apps); Magento uses OAuth 1.0a or bearer tokens; BigCommerce uses OAuth + API tokens; NetSuite uses Token-Based Authentication (TBA). [src1, src3, src5]
System Profile
This integration playbook covers connecting the three dominant ecommerce platforms -- Shopify, Adobe Commerce (Magento 2), and BigCommerce -- to major ERP systems for automated order processing and inventory synchronization. The primary data flows are: orders from ecommerce to ERP (including customer data, line items, payment status, and shipping addresses), and inventory/fulfillment data from ERP back to ecommerce (stock levels, tracking numbers, shipment status). [src2, src6]
The playbook focuses on cloud-to-cloud integrations using native APIs and iPaaS middleware. On-premise Magento installations follow the same API patterns but require network/firewall configuration not covered here. For direct CRM-to-ERP integration (e.g., Salesforce opportunity to NetSuite sales order without an ecommerce frontend), see the Order-to-Cash Integration card instead. [src3]
| System | Role | API Surface | Direction |
|---|---|---|---|
| Shopify | Ecommerce storefront -- order capture | REST Admin, GraphQL Admin, Webhooks | Outbound (orders) / Inbound (inventory) |
| Adobe Commerce (Magento 2) | Ecommerce storefront -- order capture | REST, GraphQL, Message Queues | Outbound (orders) / Inbound (inventory) |
| BigCommerce | Ecommerce storefront -- order capture | REST V2/V3, Webhooks | Outbound (orders) / Inbound (inventory) |
| Oracle NetSuite | ERP -- financial master, fulfillment | SuiteTalk SOAP, REST API, RESTlets | Inbound (orders) / Outbound (inventory, fulfillment) |
| SAP S/4HANA | ERP -- financial master, fulfillment | OData v4, IDoc | Inbound (orders) / Outbound (inventory, fulfillment) |
| D365 Business Central | ERP -- financial master, fulfillment | OData v4, REST API v2.0 | Inbound (orders) / Outbound (inventory, fulfillment) |
| iPaaS Middleware | Orchestrator -- data transformation | Celigo, Boomi, MuleSoft, Workato | Bidirectional |
API Surfaces & Capabilities
| API Surface | Platform | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|---|
| GraphQL Admin API | Shopify | HTTPS/JSON | Orders, products, inventory (recommended) | 250 items per input | 100-1,000 pts/s by plan | Yes | Via Bulk Operations |
| REST Admin API | Shopify | HTTPS/JSON | Legacy, simple CRUD | 250 per page | 2 req/s (40 bucket) | Yes | No |
| Webhooks | Shopify | HTTPS/JSON | Event-driven order capture | 1 event per call | N/A (push) | Yes | No |
| REST API | Magento 2 | HTTPS/JSON | Orders, products, inventory | 300 per page | Disabled by default | Yes | Via async endpoints |
| Async Bulk API | Magento 2 | HTTPS/JSON | High-volume inventory updates | 5,000 per request | Configurable via Redis | No | Yes |
| REST V3 API | BigCommerce | HTTPS/JSON | Orders, products, catalog | 250 per page | 150-450 req/30s | Yes | No |
| Webhooks | BigCommerce | HTTPS/JSON | Event-driven order capture | 1 event per call | N/A (push) | Yes | No |
| SuiteTalk SOAP | NetSuite | HTTPS/XML | Order creation, inventory reads | 1,000 per search | 5 concurrent (default) | Yes | Via CSV import |
| OData v4 | SAP S/4HANA | HTTPS/JSON | Sales orders, inventory | 5,000 per page | Throttled/fair-use | Yes | Via $batch |
| REST API v2.0 | D365 BC | HTTPS/JSON | Sales orders, items, inventory | 20,000 per page | Throttled/fair-use | Yes | Via $batch |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Platform | Notes |
|---|---|---|---|
| Max items per GraphQL mutation | 250 | Shopify | Use bulkOperationRunMutation for larger batches |
| Max query cost per request | 1,000 points | Shopify GraphQL | Single query cannot exceed this regardless of plan |
| Max REST response page size | 250 records | Shopify REST | Use page_info cursor pagination |
| Max records per search | 1,000 | NetSuite SuiteTalk | Use searchMoreWithId for pagination |
| Max $top per OData request | 5,000 | SAP S/4HANA | Server-driven paging for larger sets |
| Max request body | 100 MB | Magento 2 Async | For bulk async operations |
| Max webhook response time | 19 seconds | Shopify | Exceed = timeout + retry queue |
Rolling / Daily Limits
| Limit Type | Value | Window | Platform Differences |
|---|---|---|---|
| Shopify GraphQL points | 100-1,000/s | Per second (leaky bucket) | Standard: 100, Advanced: 200, Plus: 1,000, Enterprise: 2,000 |
| Shopify REST requests | 40 bucket (2/s refill) | Rolling | Shared across all apps on store |
| BigCommerce API requests | 150-450 / 30s | 30-second window | Standard/Plus: 150, Pro: 450, Enterprise: custom |
| BigCommerce webhooks | 60 / min | Per minute | Across all webhook consumers |
| NetSuite concurrent requests | 5-10 | Per account | Default 5; SuiteCloud Plus: 10+ |
| NetSuite governance units | 1,000 (scheduled) / 10,000 (map-reduce) | Per script execution | SuiteScript 2.x governance |
Authentication
| Platform | Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| Shopify | OAuth 2.0 | Public/custom apps | Permanent (offline) | No | Offline access tokens don't expire |
| Shopify | Custom App Token | Private integrations | Permanent | No | Generated in Shopify Admin |
| Magento 2 | OAuth 1.0a | Third-party integrations | Permanent (until revoked) | No | 4-step handshake required |
| Magento 2 | Bearer Token | Admin/customer API | 4 hours (admin) | No | POST /rest/V1/integration/admin/token |
| BigCommerce | OAuth + API Token | All integrations | Permanent | No | Store-level, scoped permissions |
| NetSuite | Token-Based Auth (TBA) | Server-to-server | Permanent (until revoked) | No | Recommended over credentials |
| SAP S/4HANA | OAuth 2.0 Client Credentials | Server-to-server | Configurable (default 12h) | Yes | Requires x-csrf-token for writes |
| D365 BC | OAuth 2.0 (Azure AD) | Server-to-server | 1 hour | Yes | Azure App Registration required |
Authentication Gotchas
- Shopify offline tokens never expire but can be revoked -- if a merchant uninstalls and reinstalls your app, you get a new token. Store tokens per-shop with install timestamp. [src1]
- Magento 2 OAuth 1.0a is NOT OAuth 2.0 -- you need a 4-step handshake. Most HTTP libraries need an explicit OAuth 1.0a plugin. [src5]
- NetSuite TBA requires enabling the feature -- go to Setup > Company > Enable Features > SuiteCloud > Token-Based Authentication. The integration record must also have TBA enabled separately. [src3]
- SAP S/4HANA write operations require CSRF tokens -- fetch a token via
GETwithx-csrf-token: fetchheader before everyPOST/PATCH/DELETE. [src6]
Constraints
- Inventory sync must be unidirectional (ERP to ecommerce) -- the ERP is the inventory source of truth. Writing inventory from ecommerce to ERP creates ghost stock and overselling during concurrent warehouse operations. [src3, src6]
- Shopify's leaky bucket rate limit is per-app, not per-endpoint -- all your order reads, inventory writes, and product updates share one bucket. A bulk product catalog sync can starve your order-fetching integration. [src1]
- BigCommerce webhook callbacks must respond within 3 seconds -- acknowledge immediately with 200, then process asynchronously. Slow consumers get automatically unsubscribed. [src4]
- NetSuite concurrent request limit (default 5) is per-account -- if you have multiple integrations (ecommerce + CRM + 3PL), they all share the same pool. [src3]
- Magento 2 async bulk API requires RabbitMQ or MySQL message queue -- if your hosting doesn't support message queues, async endpoints return 400. [src5]
- Order IDs are NOT globally unique across platforms -- always use platform-prefixed composite keys (e.g.,
SHOP-1001,BC-1001) as external IDs in ERP. [src3, src7]
Integration Pattern Decision Tree
START -- Ecommerce-to-ERP integration
|-- What data flow?
| |-- Orders (ecommerce to ERP)
| | |-- Volume < 100 orders/day?
| | | |-- YES: Webhook-driven (near-real-time, simplest)
| | | +-- NO:
| | |-- Volume 100-1,000 orders/day?
| | | |-- YES: Webhook + queue (RabbitMQ/SQS) then ERP batch insert
| | | +-- NO:
| | |-- Volume > 1,000 orders/day?
| | | |-- YES: Webhook + queue + iPaaS batch processing (every 5-15min)
| | | +-- Flash sale / > 10K/day? iPaaS with auto-scaling + ERP bulk import
| | +-- Need immediate ERP confirmation?
| | |-- YES: Synchronous API call (watch rate limits!)
| | +-- NO: Async webhook + queue + batch (recommended)
| |-- Inventory (ERP to ecommerce)
| | |-- SKU count < 1,000? Scheduled poll every 5-15min
| | |-- SKU count < 50,000? Delta sync (changed-since) every 5-15min
| | +-- Need < 1min latency? ERP-side CDC/webhook push
| |-- Fulfillment/Tracking (ERP to ecommerce)
| | +-- Always push-based: ERP triggers update via middleware
| +-- Product/Catalog (ERP to ecommerce)
| +-- Scheduled batch sync (hourly/daily)
|-- Which middleware?
| |-- < 100 orders/day: Direct API (no middleware needed)
| |-- 100-1,000/day: Celigo, Workato, or native connector
| |-- > 1,000/day: MuleSoft, Boomi, or Celigo scaling tier
| +-- Multi-channel: iPaaS required
+-- Error handling?
|-- Zero-loss: Idempotent writes + DLQ + reconciliation job
+-- Best-effort: Retry with exponential backoff (max 5 retries)
Quick Reference
| Step | Source System | Action | Target System | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | Shopify/Magento/BC | Order placed -- webhook fires | iPaaS Queue | Order header, line items, customer, shipping | Webhook retries (Shopify: 19x/48h) |
| 2 | iPaaS | Transform + validate order data | ERP | Sales Order / Sales Invoice | Retry 3x with backoff, then DLQ |
| 3 | iPaaS | Create/match customer record | ERP | Customer / Business Partner | Match on email; create if not found |
| 4 | iPaaS | Map SKUs to ERP item IDs | ERP | Item / Material / Product | Fail order if SKU not found (alert) |
| 5 | ERP | Allocate inventory, create pick/pack | WMS / 3PL | Fulfillment record | Manual review queue |
| 6 | ERP | Ship order, generate tracking | iPaaS | Shipment + tracking number | Retry push to ecommerce |
| 7 | iPaaS | Update order with tracking | Shopify/Magento/BC | Fulfillment / Shipment record | Retry 3x, then alert |
| 8 | ERP | Adjust inventory quantities | iPaaS | Inventory levels per location | Delta sync on schedule |
| 9 | iPaaS | Push inventory updates | Shopify/Magento/BC | Inventory quantities per SKU | Retry, then full reconciliation |
| 10 | ERP | Invoice created, payment matched | Accounting | Invoice + payment | Manual reconciliation |
Step-by-Step Integration Guide
1. Configure Webhook Listeners for Order Capture
Register webhooks on your ecommerce platform to receive order events. This eliminates polling and ensures near-real-time order flow. [src1, src4]
Shopify (GraphQL Admin API):
mutation {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://your-middleware.com/webhooks/shopify/orders"
format: JSON
}
) {
webhookSubscription { id }
userErrors { field message }
}
}
BigCommerce (REST V3):
curl -X POST https://api.bigcommerce.com/stores/{store_hash}/v3/hooks \
-H "X-Auth-Token: {api_token}" \
-H "Content-Type: application/json" \
-d '{"scope": "store/order/created", "destination": "https://your-middleware.com/webhooks/bigcommerce/orders", "is_active": true}'
Verify: Shopify Admin > Settings > Notifications > Webhooks shows new subscription; BigCommerce: GET /v3/hooks returns active hook
2. Receive and Queue Order Webhooks
Never process orders synchronously in the webhook handler. Acknowledge immediately, then queue for async processing. [src3, src7]
# Python (Flask) -- webhook receiver with queue
import hmac, hashlib, json, base64
from flask import Flask, request, jsonify
import boto3
app = Flask(__name__)
sqs = boto3.client('sqs')
QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789/ecommerce-orders"
@app.route('/webhooks/shopify/orders', methods=['POST'])
def shopify_order_webhook():
# Verify HMAC signature (critical for security)
hmac_header = request.headers.get('X-Shopify-Hmac-Sha256', '')
digest = hmac.new(b'YOUR_WEBHOOK_SECRET', request.data, hashlib.sha256).digest()
computed = base64.b64encode(digest).decode()
if not hmac.compare_digest(computed, hmac_header):
return jsonify({"error": "Invalid signature"}), 401
# Queue immediately, respond fast (< 5s)
order = json.loads(request.data)
sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps({
"platform": "shopify", "order_id": order["id"], "payload": order
}), MessageGroupId="orders")
return jsonify({"status": "queued"}), 200
Verify: Send a test order → check queue depth increases by 1
3. Transform and Map Order Data to ERP Schema
Map ecommerce order fields to ERP-specific sales order format. This is where most integrations break. [src3, src7]
# Python -- Shopify order to NetSuite Sales Order mapping
def map_shopify_to_netsuite(shopify_order):
customer = {
"email": shopify_order["email"],
"firstName": shopify_order["customer"]["first_name"],
"lastName": shopify_order["customer"]["last_name"],
"externalId": f"SHOP-{shopify_order['customer']['id']}"
}
items = []
for item in shopify_order["line_items"]:
items.append({
"item": {"externalId": item["sku"]},
"quantity": item["quantity"],
"rate": float(item["price"]),
})
sales_order = {
"externalId": f"SHOP-{shopify_order['order_number']}",
"entity": {"externalId": f"SHOP-{shopify_order['customer']['id']}"},
"tranDate": shopify_order["created_at"][:10],
"itemList": {"item": items},
}
return customer, sales_order
Verify: Run mapping on a sample order → validate all required ERP fields populated, no nulls in mandatory fields
4. Create Sales Order in ERP with Idempotency
Use external IDs to make order creation idempotent -- retrying the same order must not create duplicates. [src3]
# Python -- NetSuite REST API order creation with idempotency
def create_netsuite_order(sales_order, auth_headers):
response = requests.post(NETSUITE_URL, json=sales_order, headers={
**auth_headers, "Content-Type": "application/json",
"Prefer": "return=representation"
})
if response.status_code == 204:
return {"status": "created", "id": response.headers.get("Location")}
elif response.status_code == 409:
return {"status": "already_exists"} # Idempotent success
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
raise RateLimitError(f"Retry after {retry_after}s")
else:
raise IntegrationError(f"Error {response.status_code}: {response.text}")
Verify: GET /services/rest/record/v1/salesOrder?q=externalId IS "SHOP-1001" returns the created order
5. Sync Inventory from ERP to Ecommerce (Delta Sync)
Pull inventory changes from ERP since last sync, then push updates to ecommerce platform. Always ERP to ecommerce direction. [src2, src3]
# Python -- NetSuite to Shopify inventory sync (delta)
def sync_inventory_netsuite_to_shopify(last_sync_time):
suiteql = f"""SELECT item.itemId AS sku, il.quantityAvailable AS qty
FROM inventoryBalance il JOIN item ON il.item = item.id
WHERE item.lastModifiedDate > '{last_sync_time}'"""
changed_items = netsuite_query(suiteql)
for item in changed_items:
shopify_graphql("inventorySetQuantities", {
"name": "available", "reason": "correction",
"quantities": [{"inventoryItemId": sku_to_id(item["sku"]),
"locationId": LOCATION_ID, "quantity": int(item["qty"])}]
})
Verify: Change quantity in NetSuite → run sync → Shopify Admin shows updated stock
6. Push Fulfillment and Tracking Back to Ecommerce
When the ERP ships an order, push tracking information back to the ecommerce platform. [src3]
// Node.js -- Push NetSuite fulfillment to Shopify
async function pushFulfillmentToShopify(orderId, trackingNumber, carrier) {
const shopifyOrderId = orderId.replace('SHOP-', '');
const foResponse = await axios.get(
`https://${SHOP}.myshopify.com/admin/api/2025-01/orders/${shopifyOrderId}/fulfillment_orders.json`,
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
);
const fulfillmentOrderId = foResponse.data.fulfillment_orders[0].id;
await axios.post(
`https://${SHOP}.myshopify.com/admin/api/2025-01/fulfillments.json`,
{ fulfillment: {
line_items_by_fulfillment_order: [{ fulfillment_order_id: fulfillmentOrderId }],
tracking_info: { number: trackingNumber, company: carrier },
notify_customer: true
}},
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
);
}
Verify: GET /admin/api/2025-01/orders/{id}/fulfillments.json shows status: success with tracking_number
Code Examples
Python: Shopify Bulk Inventory Update via GraphQL Bulk Operations
# Input: Dict of SKU to quantity mappings from ERP export
# Output: Shopify inventory levels updated for all changed SKUs
import requests, json, time
SHOP = "your-store.myshopify.com"
TOKEN = "shpat_xxxxx"
HEADERS = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}
API_URL = f"https://{SHOP}/admin/api/2025-01/graphql.json"
def bulk_inventory_update(sku_qty_map, location_id):
# Build JSONL for bulk mutation
jsonl_lines = [json.dumps({"input": {"name": "available", "reason": "correction",
"quantities": [{"inventoryItemId": sku_to_id(sku),
"locationId": f"gid://shopify/Location/{location_id}",
"quantity": qty}]}}) for sku, qty in sku_qty_map.items()]
# Stage upload, then run bulk mutation
# ... (see full code in .md file)
JavaScript/Node.js: BigCommerce Order Fetch with Rate Limit Handling
// Input: BigCommerce store credentials + date range
// Output: Array of order objects for ERP transformation
async function fetchOrdersWithRateLimit(sinceDate) {
let allOrders = [], page = 1, hasMore = true;
while (hasMore) {
try {
const response = await axios.get(`${BC_API}/orders`, {
headers: HEADERS,
params: { min_date_created: sinceDate, limit: 250, page }
});
allOrders = allOrders.concat(response.data.data);
const remaining = parseInt(response.headers['x-rate-limit-requests-left'] || '100');
if (remaining < 10) {
const resetMs = parseInt(response.headers['x-rate-limit-time-reset-ms'] || '5000');
await new Promise(r => setTimeout(r, resetMs));
}
hasMore = response.data.data.length === 250;
page++;
} catch (err) {
if (err.response?.status === 429) {
const resetMs = parseInt(err.response.headers['x-rate-limit-time-reset-ms'] || '15000');
await new Promise(r => setTimeout(r, resetMs));
} else throw err;
}
}
return allOrders;
}
cURL: Quick API Tests for Each Platform
# Shopify: Test auth + fetch recent orders
curl -s "https://YOUR-STORE.myshopify.com/admin/api/2025-01/orders.json?limit=5&status=any" \
-H "X-Shopify-Access-Token: shpat_XXXXX" | jq '.orders | length'
# BigCommerce: Test auth + fetch orders
curl -s "https://api.bigcommerce.com/stores/STORE_HASH/v3/orders?limit=5" \
-H "X-Auth-Token: YOUR_TOKEN" | jq '.data | length'
# Magento 2: Get bearer token + fetch orders
TOKEN=$(curl -s -X POST "https://your-magento.com/rest/V1/integration/admin/token" \
-H "Content-Type: application/json" -d '{"username":"admin","password":"pass"}' | tr -d '"')
curl -s "https://your-magento.com/rest/V1/orders?searchCriteria[pageSize]=5" \
-H "Authorization: Bearer $TOKEN" | jq '.items | length'
Data Mapping
Field Mapping Reference
| Source Field (Ecommerce) | NetSuite Target | SAP S/4HANA Target | D365 BC Target | Type | Gotcha |
|---|---|---|---|---|---|
| order.id | salesOrder.externalId | SalesOrder.PurchaseOrderByCustomer | salesOrder.externalDocumentNumber | String | Prefix with platform code (SHOP-, BC-, MAG-) |
| order.order_number | salesOrder.otherRefNum | SalesOrder.CustomerPurchaseOrderNumber | salesOrder.number | String | Shopify order_number != id |
| order.created_at | salesOrder.tranDate | SalesOrder.SalesOrderDate | salesOrder.orderDate | Date | Convert timezone: Shopify=UTC, NetSuite=user pref |
| order.total_price | salesOrder.total | SalesOrder.TotalNetAmount | salesOrder.totalAmountIncludingTax | Decimal | Verify 2 vs 4 decimal places |
| order.currency | salesOrder.currency.externalId | SalesOrder.TransactionCurrency | salesOrder.currencyCode | String | NetSuite uses internal IDs, not ISO codes by default |
| line_items[].sku | item.externalId | SalesOrderItem.Material | salesLine.itemNo | String | THE #1 FAILURE POINT -- must match exactly |
| line_items[].quantity | item.quantity | SalesOrderItem.RequestedQuantity | salesLine.quantity | Integer | Check UOM mapping (each vs case) |
| line_items[].price | item.rate | SalesOrderItem.NetAmount | salesLine.unitPrice | Decimal | Tax-inclusive (Shopify EU) vs tax-exclusive (ERPs) |
| customer.email | customer.email | BusinessPartner.EmailAddress | customer.email | String | Primary match key for deduplication |
| shipping_address.zip | shippingAddress.zip | ShipToParty.PostalCode | shipToAddress.postalCode | String | International formats vary (UK: SW1A 1AA) |
Data Type Gotchas
- Tax-inclusive vs tax-exclusive pricing: Shopify stores prices tax-inclusive by default in EU/UK/AU stores but tax-exclusive in US. NetSuite and SAP typically store tax-exclusive amounts. [src3]
- Currency decimal precision: Shopify uses 2 decimal places for most currencies but some (JPY, KRW) use 0. SAP stores in smallest currency unit (cents). [src7]
- Date/timezone handling: Shopify timestamps are UTC (ISO 8601). NetSuite dates depend on user timezone preference. Always convert to the ERP's expected timezone. [src3]
- Shopify variant IDs vs SKUs: A Shopify product has variants with both variant_id (numeric) and sku (string). Always map on SKU, never on variant_id -- variant IDs change if the product is recreated. [src1]
Error Handling & Failure Points
Common Error Codes
| Code | Platform | Meaning | Cause | Resolution |
|---|---|---|---|---|
| 429 | All | Rate limit exceeded | Too many API calls in window | Exponential backoff: wait 2^n * 1s, max 5 retries. Read Retry-After header |
| 422 | Shopify | Unprocessable Entity | Invalid data (missing required field) | Check response body for field-level errors; fix data mapping |
| 401 | All | Unauthorized | Expired or invalid token | Re-authenticate; check app isn't uninstalled |
| 409 | NetSuite | Conflict / Duplicate | externalId already exists | Idempotent -- this is success for retry scenarios |
| 400 | Magento | Bad Request | Invalid search criteria or missing queue | Verify searchCriteria syntax; check RabbitMQ running |
| RCRD_DSNT_EXIST | NetSuite | Record not found | Referenced item/customer doesn't exist | Verify SKU mapping; create missing master data first |
Failure Points in Production
- SKU not found in ERP: Orders with unmapped SKUs silently fail or create orders with missing line items. Fix:
Implement a SKU validation step before order creation -- reject with alert if any SKU missing. [src3] - Webhook deduplication failure: Shopify can send the same webhook event multiple times. Fix:
Store processed webhook IDs (X-Shopify-Webhook-Id) in Redis with 48h TTL; skip if already processed. [src1] - OAuth token revocation during peak traffic: Merchant reinstalls app or changes permissions mid-sync. Fix:
Catch 401 errors, trigger re-auth flow, queue failed orders for retry. [src2] - Partial fulfillment mismatch: ERP ships partial order but ecommerce expects full fulfillment. Fix:
Map ERP partial shipments to partial fulfillments; track state per line item. [src3] - Inventory count drift over time: Small discrepancies accumulate even with delta sync. Fix:
Run full inventory reconciliation job weekly -- compare all quantities and correct mismatches. [src6] - Flash sale traffic kills integration: Thousands of webhooks arrive simultaneously. Fix:
Use message queue as buffer; auto-scale consumers; implement circuit breaker on ERP API calls. [src3]
Anti-Patterns
Wrong: Polling ecommerce platform for new orders every 60 seconds
# BAD -- wastes API quota, adds latency, hits rate limits at scale
while True:
orders = shopify.get("/orders.json?status=any&created_at_min=" + last_check)
for order in orders:
create_erp_order(order)
time.sleep(60)
Correct: Use webhooks with queue-based processing
# GOOD -- event-driven, near-real-time, respects rate limits
@app.route('/webhooks/shopify/orders', methods=['POST'])
def handle_order_webhook():
verify_hmac(request)
queue.enqueue(request.data) # Queue immediately
return '', 200 # Respond fast
# Queue consumer processes orders at sustainable rate
Wrong: Bidirectional inventory sync without conflict resolution
# BAD -- creates infinite loops and ghost stock
def sync_inventory():
shopify_qty = get_shopify_inventory(sku)
netsuite_qty = get_netsuite_inventory(sku)
if shopify_qty != netsuite_qty:
update_netsuite(sku, shopify_qty) # Wrong! Shopify is not source of truth
update_shopify(sku, netsuite_qty) # Overwrites what you just pushed
Correct: Unidirectional inventory sync (ERP as source of truth)
# GOOD -- ERP is single source of truth for inventory
def sync_inventory():
changed_items = query_netsuite_changed_since(last_sync_time)
for item in changed_items:
update_shopify_inventory(sku=item["sku"], quantity=item["quantityAvailable"])
save_last_sync_time(datetime.utcnow())
Wrong: Hardcoded SKU mapping in code
# BAD -- breaks every time a new product is added
SKU_MAP = {"BLK-SHIRT-L": "10042", "WHT-SHIRT-M": "10043"} # 2000 more...
Correct: Dynamic SKU lookup with ERP external IDs
# GOOD -- SKU matching via ERP external ID field
def get_erp_item_id(sku):
result = netsuite_search("item", {"externalId": sku})
if not result:
raise SKUNotFoundError(f"SKU '{sku}' not found in ERP")
return result["internalId"]
Common Pitfalls
- SKU format mismatch between platforms: Shopify uses
BLK-SHIRT-L, ERP usesBLACK_SHIRT_LARGE. Fix:Create a SKU mapping table maintained in iPaaS or shared database. Validate all SKUs exist before processing orders.[src3, src7] - Ignoring Shopify's webhook HMAC verification: Skipping signature verification exposes integration to spoofed orders. Fix:
Always verify X-Shopify-Hmac-Sha256 header. BigCommerce doesn't sign webhooks -- use IP allowlisting.[src1] - Not handling order edits after creation: Merchants edit orders after creation. Fix:
Subscribe to orders/updated webhook in addition to orders/create. Implement order amendment logic in ERP.[src2] - Testing with production API limits: Sandbox/dev stores have different rate limits. Fix:
Load-test against representative environment. Simulate peak traffic before go-live.[src1] - Assuming tax calculation matches: Shopify calculates tax at checkout; ERP recalculates with its own engine. Fix:
Decide one source of truth for tax. Either pass Shopify tax as override, or accept small rounding differences.[src3] - No dead letter queues for failed orders: Failed orders disappear silently. Fix:
After max retries, move to DLQ with original payload + error details. Build admin dashboard for review.[src7] - Single-threaded webhook processing: Sequential processing creates bottleneck during peak. Fix:
Use competing consumers pattern -- multiple workers from same queue. Scale based on queue depth.[src6]
Diagnostic Commands
# Shopify: Check API usage / remaining limits
curl -sI "https://YOUR-STORE.myshopify.com/admin/api/2025-01/shop.json" \
-H "X-Shopify-Access-Token: TOKEN" | grep -i "x-shopify"
# Look for: X-Shopify-Shop-Api-Call-Limit: 2/40
# BigCommerce: Check rate limit headers
curl -sI "https://api.bigcommerce.com/stores/HASH/v3/catalog/products?limit=1" \
-H "X-Auth-Token: TOKEN" | grep -i "x-rate-limit"
# Shopify: Verify webhook registrations
curl -s "https://YOUR-STORE.myshopify.com/admin/api/2025-01/webhooks.json" \
-H "X-Shopify-Access-Token: TOKEN" | jq '.webhooks[] | {id, topic, address}'
# Magento: Check integration token validity
curl -s "https://your-magento.com/rest/V1/store/storeConfigs" \
-H "Authorization: Bearer YOUR_TOKEN" | jq '.[0].code'
# Shopify: Verify inventory levels for a SKU
curl -s "https://YOUR-STORE.myshopify.com/admin/api/2025-01/inventory_levels.json?inventory_item_ids=ID" \
-H "X-Shopify-Access-Token: TOKEN" | jq '.inventory_levels[] | {location_id, available}'
Version History & Compatibility
| Component | Current Version | Release Date | Status | Breaking Changes | Notes |
|---|---|---|---|---|---|
| Shopify Admin API | 2025-01 | 2025-01 | Current | REST deprecation push toward GraphQL | Versioned endpoints; 12mo min support |
| Shopify Admin API | 2024-10 | 2024-10 | Supported | N/A | Previous stable version |
| Adobe Commerce | 2.4.7 | 2024-04 | Current | Async bulk API improvements | PHP 8.2+ required |
| BigCommerce REST API | V3 | 2020 (ongoing) | Current | V2 being deprecated | Always use V3 where available |
| NetSuite REST API | 2024.2 | 2024-08 | Current | N/A | SOAP still supported, REST preferred |
| SAP S/4HANA OData | 2408 | 2024-08 | Current | Async OData processing | x-csrf-token required for writes |
| D365 Business Central | API v2.0 | 2023 (ongoing) | Current | N/A | OData v4 with Azure AD auth |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Automating order flow from Shopify/Magento/BigCommerce into any ERP | You only sell on one marketplace (Amazon/eBay) | Marketplace-specific integration patterns |
| Need real-time inventory visibility across online + warehouse | ERP doesn't have an API (legacy on-premise) | File-based integration (CSV/SFTP drop) |
| Processing 100-10,000+ orders/day | < 10 orders/day and manual entry is acceptable | Manual order entry in ERP |
| Running multi-channel ecommerce (multiple storefronts) | Single Shopify store with native inventory | Shopify's built-in inventory management |
| Need automated fulfillment + tracking number pushback | ERP is only for financials, not fulfillment | Simple accounting sync (journal entries only) |
Cross-System Comparison
| Capability | Shopify | Adobe Commerce (Magento 2) | BigCommerce | Notes |
|---|---|---|---|---|
| API Style | REST + GraphQL | REST + GraphQL + SOAP | REST (V2/V3) + GraphQL (Storefront) | Shopify pushing GraphQL as primary |
| Rate Limits | 100-1,000 pts/s (GraphQL) | Disabled by default (configurable) | 150-450 req/30s by plan | Magento most permissive; Shopify most structured |
| Webhook Support | Excellent (HMAC signed) | Via observers + message queue | Good (no signature) | Shopify webhooks most reliable |
| Bulk Operations | GraphQL Bulk Operations | Async bulk REST endpoints | No native bulk API | Shopify best for large catalog updates |
| Auth Model | OAuth 2.0 / API key | OAuth 1.0a / bearer token | OAuth + API token | Magento's OAuth 1.0a is oldest pattern |
| Inventory API | inventorySetQuantities (GraphQL) | POST /V1/inventory/source-items | PUT /v3/inventory/items | Shopify: multi-location native |
| Order API | Orders + Fulfillment Orders | Orders + Shipments | Orders V2/V3 + Shipments | Shopify fulfillment_orders most flexible |
| Multi-store | Shopify Markets (single admin) | Multi-website architecture | Multi-storefront | Magento most flexible for B2B |
| Native ERP Connectors | Global ERP Program | Adobe Commerce connectors | App marketplace | Shopify most mature partner program |
| Pagination | Cursor-based (page_info) | Page-based (searchCriteria) | Page-based (page + limit) | Cursor-based most scalable |
Important Caveats
- Rate limits differ dramatically by plan tier -- a Shopify Basic store gets 100 GraphQL points/s while Plus gets 1,000/s. Design your integration for the lowest plan you'll support, or build adaptive throttling. [src1]
- Webhook reliability is not 100% -- Shopify retries failed webhooks up to 19 times over 48 hours, then stops. BigCommerce auto-unsubscribes slow consumers. Always run a reconciliation job to catch missed events. [src1, src4]
- iPaaS costs add up -- Celigo ~$16,500/year, Jitterbit ~$19,600/year, Boomi ~$300/user/month. For < 100 orders/day, direct API integration may be more cost-effective. [src3]
- Shopify is deprecating REST in favor of GraphQL -- while REST Admin API still works, new features are GraphQL-only. Plan migration for long-term maintenance. [src1]
- Test during simulated peak load -- 48% of retailers lose > $50,000/year due to poor integration, typically manifesting during peak traffic (Black Friday, flash sales). [src7]
- This card covers API integration patterns, not specific iPaaS configuration -- for step-by-step Celigo/Boomi/MuleSoft setup, consult the respective platform's connector documentation. [src6]