
Erp Integration
- 92 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Sync orders, inventory, and customers between your store and an ERP like SAP, NetSuite, or Odoo using middleware and async queues.
About
Covers integration architecture patterns, data mapping, conflict resolution, and idempotent sync between commerce and ERP systems. A developer uses it for automated ERP order fulfillment or real-time inventory sync from an ERP/WMS.
- Event-driven, polling, and middleware architecture patterns
- Idempotent sync with retry strategies to prevent duplicate records
Erp Integration by the numbers
- 92 all-time installs (skills.sh)
- Ranked #3,011 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill erp-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 92 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Sync orders, inventory, and customers between your store and an ERP like SAP, NetSuite, or Odoo using middleware and async queues.
Files
ERP Integration
Overview
Build integrations between e-commerce platforms and ERP systems (SAP, NetSuite, Odoo, Microsoft Dynamics) for bidirectional sync of orders, inventory, customers, and products. This skill covers integration architecture patterns (event-driven, polling, middleware), data mapping, conflict resolution, error handling with retry strategies, and idempotent sync that prevents duplicate records.
When to Use This Skill
- When connecting a storefront to an ERP for automated order fulfillment
- When syncing real-time inventory levels from an ERP/WMS to the e-commerce catalog
- When building customer master data sync between the storefront and ERP
- When implementing product and pricing feeds from the ERP to the storefront
- When designing a middleware layer to handle multiple integration points
Core Instructions
Step 1: Determine your platform and integration approach
| Platform | Integration Option | Recommended Approach |
|---|---|---|
| Shopify | Shopify Admin API + webhooks for order data | Use Zapier (no-code, $20/month) or Celigo (iPaaS) for standard ERP connectors; for NetSuite use the official NetSuite Connector for Shopify app; for custom needs use Shopify webhooks |
| WooCommerce | WooCommerce REST API + WordPress hooks | Use Zapier for simple flows; install the Zynk WooCommerce connector (from £500) for SAP/NetSuite; or build a custom integration using WooCommerce's REST API |
| BigCommerce | BigCommerce API + webhooks | Use Celigo or Boomi for enterprise ERP connectors; BigCommerce has pre-built connectors for NetSuite, SAP, and Microsoft Dynamics in the App Marketplace |
| Custom / Headless | Full API access — build a middleware service | Implement event-driven order sync (store webhooks → queue → ERP adapter), polling-based inventory sync (scheduled job → ERP API → update catalog), and a dead-letter queue for failed syncs |
Step 2: Platform-specific ERP integration
---
Shopify
Use a pre-built connector for standard ERPs:
1. For NetSuite: Install the official NetSuite Connector for Shopify from the Shopify App Store ($150-$300/month). It syncs orders, inventory, and customers bidirectionally with no custom code 2. For SAP: Use Celigo's SAP + Shopify integration template or contact your SAP partner for their Shopify connector 3. For Odoo: Install the Odoo Shopify Connector module in Odoo (free, community edition) — configure your Shopify API credentials in Odoo's settings
For custom ERP connections using Shopify webhooks:
1. In your Shopify admin, go to Settings → Notifications → Webhooks 2. Click Create webhook and add endpoints for orders/create, orders/paid, and inventory_levels/update 3. Your ERP middleware endpoint receives order data in JSON and transforms it to the ERP's format 4. For inventory sync back to Shopify, use the Inventory API to update levels after each ERP poll
---
WooCommerce
Use Zapier for simple, low-volume ERP sync:
1. Connect WooCommerce and your ERP (NetSuite, Odoo, Sage) in zapier.com 2. Create a Zap: trigger = New Order in WooCommerce, action = Create Sales Order in NetSuite 3. Map the WooCommerce order fields to your ERP's required fields in Zapier's field mapper 4. Zapier polls WooCommerce every 15 minutes on the free plan; upgrade to Starter ($19.99/month) for faster polling
For higher volume or custom ERP connections:
1. Install WP Webhooks (free, wordpress.org) to send WooCommerce events to your middleware 2. Use the WooCommerce REST API (/wp-json/wc/v3/orders) with OAuth 1.0a for your middleware to pull orders 3. For inventory sync from ERP to WooCommerce, use the WooCommerce Products API (PUT /wp-json/wc/v3/products/{id}) to update stock_quantity
---
Custom / Headless
Integration architecture — choose the pattern that fits your ERP:
Event-Driven (recommended for real-time order sync):
Storefront → Webhook/Event → Message Queue (SQS/BullMQ) → ERP Adapter
Polling (for ERPs without webhooks, like legacy SAP installations):
Scheduler → Poll ERP API → Transform → Update Storefront
Middleware Platform (for complex multi-system environments):
Storefront ↔ Celigo / MuleSoft / Workato ↔ ERPIdempotent order sync service:
// lib/erp/order-sync.ts
export async function syncOrder(orderId: string): Promise<void> {
const order = await db.orders.getWithItems(orderId);
// Check if already synced
const existingSync = await db.syncLog.findByOrderId(orderId);
if (existingSync?.status === 'synced') return;
// Check ERP for existing record (guards against retries after partial failure)
const existing = await erpAdapter.findOrderByExternalReference(order.orderNumber);
if (existing) {
await db.syncLog.upsert({ orderId, externalId: existing.erpOrderId, status: 'synced' });
return;
}
try {
const erpOrder = mapOrderToERP(order);
const { erpOrderId } = await erpAdapter.createSalesOrder(erpOrder);
await db.syncLog.upsert({ orderId, externalId: erpOrderId, status: 'synced', syncedAt: new Date() });
await db.orders.updateMetadata(orderId, { erpOrderId });
} catch (error) {
await db.syncLog.upsert({ orderId, status: 'failed', lastError: error.message });
throw error; // Let the retry mechanism handle it
}
}
function mapOrderToERP(order: Order): ERPSalesOrder {
return {
externalReference: order.orderNumber,
orderDate: order.createdAt.toISOString().split('T')[0],
customer: {
externalId: order.customer?.erpCustomerId || null,
email: order.email,
name: `${order.shippingAddress.firstName} ${order.shippingAddress.lastName}`,
},
shippingAddress: {
line1: order.shippingAddress.street1,
city: order.shippingAddress.city,
state: order.shippingAddress.state,
postalCode: order.shippingAddress.postalCode,
country: order.shippingAddress.country,
},
lineItems: order.lineItems.map(item => ({
sku: item.sku,
quantity: item.quantity,
unitPrice: item.unitPrice / 100, // Convert cents to dollars for ERP
taxAmount: item.taxAmount / 100,
})),
orderTotal: order.totalPrice / 100,
currency: order.currency,
};
}BullMQ queue for reliable order sync with exponential backoff retries:
import { Queue, Worker, QueueEvents } from 'bullmq';
const orderSyncQueue = new Queue('order-sync', {
connection: { host: process.env.REDIS_HOST, port: 6379 },
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 5000 }, // 5s, 10s, 20s, 40s, 80s
removeOnComplete: { count: 1000 },
removeOnFail: { count: 5000 },
},
});
// Producer: enqueue when order is placed
export async function onOrderPlaced(orderId: string) {
await orderSyncQueue.add('sync-order', { orderId }, {
jobId: `order-sync-${orderId}`, // Prevents duplicate queue entries
});
}
// Consumer: process sync
new Worker('order-sync', async (job) => {
await syncOrder(job.data.orderId);
}, { connection: { host: process.env.REDIS_HOST, port: 6379 }, concurrency: 5 });Inventory sync (polling-based, ERP to storefront):
// lib/erp/inventory-sync.ts — run via scheduled job every 5 minutes
export async function syncInventoryLevels(): Promise<void> {
const lastSyncAt = await redis.get('erp:inventory:last_sync');
let page = 1, hasMore = true;
while (hasMore) {
const { items, hasMore: more } = await erpAdapter.getInventoryLevels({
page, pageSize: 500,
modifiedSince: lastSyncAt ? new Date(lastSyncAt) : undefined,
});
for (const item of items) {
// Available = On Hand - Reserved - Safety Stock
const available = Math.max(0, item.onHandQuantity - item.reservedQuantity - (item.safetyStock || 0));
const current = await db.inventory.getQuantityBySku(item.sku);
if (current === available) continue; // Skip unchanged
await db.inventory.updateBySku(item.sku, { quantity: available, lastSyncedAt: new Date() });
// Update Redis cache for real-time product page availability
const productId = await db.inventory.getProductIdBySku(item.sku);
if (productId) await redis.setex(`inventory:${productId}`, 3600, String(available));
}
hasMore = more;
page++;
}
await redis.set('erp:inventory:last_sync', new Date().toISOString());
}Best Practices
- Make every sync operation idempotent — use external references (order number, SKU) to check for existing ERP records before creating; this prevents duplicates from retries
- Use a message queue for order sync — never call the ERP synchronously during checkout; enqueue and process asynchronously with retries
- Implement a dead-letter queue — after all retries are exhausted, move failed jobs to a DLQ for manual inspection; never silently drop messages
- Use delta sync, not full sync — query the ERP for records modified since the last sync timestamp; full syncs don't scale past a few thousand records
- Store the ERP record ID on your local records — after syncing an order, save the ERP order ID on your record for cross-referencing and future status lookups
- Calculate available inventory correctly —
available = onHand - reserved - safetyStockand clamp to zero; never use raw on-hand quantity from the ERP
Common Pitfalls
| Problem | Solution |
|---|---|
| Duplicate orders in ERP from retry logic | Use the e-commerce order number as an external reference and check for its existence before creating; most ERPs support duplicate-check on external IDs |
| Inventory quantities go negative after sync | Clamp available quantity to zero; Math.max(0, onHand - reserved - safetyStock) |
| ERP rate limits cause sync failures | Implement per-operation rate limiters (token bucket) and use the ERP's bulk/feed API for large batches instead of individual item calls |
| Price sync overwrites promotional prices | Separate base prices (from ERP) from promotional prices (managed in your commerce platform); never let ERP sync overwrite active promotions |
| Large initial data load times out | Break the initial sync into batches with checkpointing so you can resume after a failure; process in parallel with concurrency limits |
Related Skills
- @webhook-architecture
- @marketplace-connectors
- @monitoring-alerting-commerce
- @product-information-management
{
"context": "Tests whether the agent implements async ERP order sync correctly using BullMQ with exponential backoff retries, idempotent job enqueuing, a dead letter queue for exhausted retries, and proper ops alerting — without making synchronous ERP calls during checkout.",
"type": "weighted_checklist",
"checklist": [
{
"name": "BullMQ queue usage",
"max_score": 10,
"description": "Uses BullMQ (imports Queue and/or Worker from 'bullmq') for the order sync queue rather than a simple in-process retry loop or alternative queue library"
},
{
"name": "Async enqueue on order placed",
"max_score": 10,
"description": "Enqueues the sync job asynchronously when an order is placed, rather than calling the ERP adapter directly inline during the order placement flow"
},
{
"name": "Idempotent job ID",
"max_score": 10,
"description": "Sets a deterministic jobId derived from the orderId (e.g., 'order-sync-{orderId}') when adding jobs to the queue"
},
{
"name": "Retry attempts count",
"max_score": 8,
"description": "Configures queue job attempts to 5 (maxRetries: 5 or attempts: 5)"
},
{
"name": "Exponential backoff type",
"max_score": 8,
"description": "Configures backoff type as 'exponential' (not 'fixed' or 'linear')"
},
{
"name": "Backoff initial delay",
"max_score": 6,
"description": "Sets initial backoff delay to 5000ms (5 seconds)"
},
{
"name": "Worker concurrency",
"max_score": 6,
"description": "Sets worker concurrency to 5 (processes 5 orders in parallel)"
},
{
"name": "Dead letter queue",
"max_score": 10,
"description": "Creates a separate dead letter queue (e.g., 'order-sync-dlq') and moves jobs there after all retries are exhausted"
},
{
"name": "Ops alert on DLQ",
"max_score": 8,
"description": "Sends an alert to an ops channel (Slack, PagerDuty, email, etc.) when a job is moved to the DLQ"
},
{
"name": "DLQ manual retry endpoint",
"max_score": 8,
"description": "Provides an admin/API endpoint that re-enqueues a DLQ job to the main queue and removes it from the DLQ"
},
{
"name": "ERP record ID stored on order",
"max_score": 8,
"description": "Saves the ERP-assigned order ID back onto the local order record (or sync log) after a successful sync"
},
{
"name": "Sync log idempotency check",
"max_score": 8,
"description": "Checks the sync log or database before processing: skips the sync if the order is already marked as 'synced'"
}
]
}
Reliable Order Sync to ERP
Problem/Feature Description
GardenHarvest, a mid-size online retailer of gardening supplies, recently rolled out a new headless e-commerce storefront and connected it to their existing NetSuite ERP. Their engineering team quickly ran into a painful problem: whenever the ERP was slow or briefly unavailable during peak checkout hours, orders failed to sync, leaving operations staff scrambling to manually re-enter orders into NetSuite and customers without fulfillment updates.
The team has decided to decouple order syncing from the checkout flow entirely. They want a robust background processing system that picks up newly placed orders, attempts to push them to the ERP, and gracefully handles failures with automatic retries. They also need visibility into orders that could not be synced after all retry attempts — these must be preserved and accessible for manual intervention by the ops team, rather than simply being lost.
Output Specification
Implement the order sync background processing system in TypeScript. Produce the following files:
src/queues/orderSyncQueue.ts— Queue setup, job producer function (called when an order is placed), and worker configurationsrc/services/orderSyncService.ts— The sync service logic: fetches the order, checks for prior sync, calls the ERP adapter, updates the sync log, and stores the ERP reference on the ordersrc/queues/deadLetterQueue.ts— Dead letter queue setup, handler for moving exhausted jobs there, and alert logicsrc/routes/adminIntegrations.ts— Admin API endpoint(s) for managing stuck jobs from the dead letter queue
Include a src/types.ts with relevant TypeScript interfaces (OrderSyncPayload, RetryPolicy, SyncLogEntry, etc.).
Write a brief DESIGN.md explaining the architecture decisions made, including how retries work and how the dead letter queue fits in.
{
"context": "Tests whether the agent implements a clean ERP adapter interface with SAP-specific patterns (CSRF token fetching, padded order item numbers), correct cents-to-dollars currency conversion, a separate data transformation layer, and proper handling of base vs promotional prices.",
"type": "weighted_checklist",
"checklist": [
{
"name": "ERPAdapter interface",
"max_score": 8,
"description": "Defines an abstract ERPAdapter interface (or abstract class) with distinct methods for orders, inventory, customers, and products — not a single generic 'call ERP' method"
},
{
"name": "Separate mapper/transformer",
"max_score": 10,
"description": "Implements a dedicated mapping or transformation function/class to convert e-commerce order format to ERP format, separate from the adapter's HTTP call logic"
},
{
"name": "SAP CSRF token fetch",
"max_score": 12,
"description": "Fetches the SAP CSRF token by first making a HEAD/GET request with 'X-CSRF-Token: Fetch' header before submitting mutating requests"
},
{
"name": "CSRF token on mutating requests",
"max_score": 8,
"description": "Includes the retrieved CSRF token value in the 'X-CSRF-Token' header of POST/PUT requests to SAP"
},
{
"name": "SAP order item numbering",
"max_score": 10,
"description": "Numbers SAP sales order line items starting at 10 and incrementing by 10 (000010, 000020, ...) zero-padded to 6 digits"
},
{
"name": "Cents to dollars conversion",
"max_score": 10,
"description": "Divides monetary values (unit price, tax, shipping, discounts) by 100 when converting from e-commerce (cents) to ERP (dollars/base currency) format"
},
{
"name": "Base price vs promotional price separation",
"max_score": 10,
"description": "Keeps ERP base prices and e-commerce promotional/discount prices separate; does NOT overwrite the base price with a discounted price during sync"
},
{
"name": "Rate limiting in adapter",
"max_score": 8,
"description": "Implements or references rate limiting (token bucket, sliding window, or delay between batched calls) within the ERP adapter to avoid API throttling"
},
{
"name": "External reference for idempotency",
"max_score": 8,
"description": "Uses the e-commerce order number (or another external reference) as a duplicate-check field when creating orders in the ERP"
},
{
"name": "SAP OData API path",
"max_score": 8,
"description": "Uses the SAP OData API path /sap/opu/odata/sap for SAP requests"
},
{
"name": "ERP-specific code isolated",
"max_score": 8,
"description": "SAP-specific field names (SalesOrderType, SoldToParty, etc.) appear only inside the SAP adapter implementation, not in shared e-commerce business logic"
}
]
}
SAP S/4HANA Adapter and Order Mapping Layer
Problem/Feature Description
NordLight, a Scandinavian outdoor lighting brand, is migrating from a legacy monolith to a headless commerce platform. Their ERP of record is SAP S/4HANA, which the operations, finance, and logistics teams rely on for all order fulfillment and reporting. The new integration must push orders from the storefront into SAP as sales orders, but the engineering team has learned through painful experience that SAP's API has specific quirks — authentication requirements, strict field formats, and rate limits — that tripped up their first integration attempt.
The team wants to build a clean integration layer: a SAP-specific adapter that hides all SAP API details from the rest of the application, a data transformation layer that converts the e-commerce order format into SAP's expected format, and clear handling of pricing so that SAP's base prices and the storefront's promotional discounts don't interfere with each other. The adapter should also be designed for the long term — the team may need to add a NetSuite adapter next quarter, so the interface should be generic enough to support multiple ERP backends.
Output Specification
Implement the ERP adapter layer in TypeScript. Produce the following files:
src/adapters/ERPAdapter.ts— The abstract ERPAdapter interface defining all methodssrc/adapters/SAPAdapter.ts— The SAP S/4HANA implementation of ERPAdaptersrc/mappers/orderMapper.ts— The transformation functions that convert e-commerce order objects to ERP-generic format (and SAP-specific format where needed)src/types.ts— Shared TypeScript types: ERPSalesOrder, ERPCustomer, ERPInventoryItem, ERPProduct, SAPConfig, etc.
Write a DESIGN.md that explains:
- How the adapter pattern keeps SAP-specific code isolated
- How pricing is handled to avoid overwriting promotional prices with ERP base prices
- How the SAP adapter handles API authentication requirements
{
"context": "Tests whether the agent builds an ERP inventory sync service that uses delta polling (modifiedSince), correct available quantity calculation, paginated fetching, Redis caching with the right key pattern and TTL, and skips no-op updates.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Delta sync via modifiedSince",
"max_score": 10,
"description": "Passes a 'modifiedSince' (or equivalent) timestamp parameter to the ERP inventory query so only changed records are fetched, not the full catalog"
},
{
"name": "Last sync timestamp persistence",
"max_score": 8,
"description": "Stores and retrieves the last sync timestamp (e.g., in Redis or a database) so subsequent runs can use it as the delta cutoff"
},
{
"name": "Paginated ERP fetch",
"max_score": 8,
"description": "Iterates through paginated results from the ERP (uses a loop/while with page increment or cursor) rather than assuming a single response"
},
{
"name": "Page size 500",
"max_score": 8,
"description": "Sets the inventory page size to 500 when querying the ERP"
},
{
"name": "Available qty formula",
"max_score": 12,
"description": "Calculates available quantity as onHandQuantity minus reservedQuantity minus safetyStock (all three terms subtracted)"
},
{
"name": "Qty clamped to zero",
"max_score": 10,
"description": "Clamps the calculated available quantity to a minimum of 0 (uses Math.max(0, ...) or equivalent)"
},
{
"name": "Skip unchanged qty",
"max_score": 8,
"description": "Compares the calculated quantity against the current stored quantity and skips the update if they are equal"
},
{
"name": "Redis cache update",
"max_score": 8,
"description": "Writes the updated inventory quantity to Redis after a database update"
},
{
"name": "Redis key pattern",
"max_score": 10,
"description": "Uses a Redis key of the form 'inventory:{productId}' (not SKU-based) for the cached value"
},
{
"name": "Redis TTL 3600",
"max_score": 10,
"description": "Sets a TTL of 3600 seconds (1 hour) on the Redis inventory cache entry"
},
{
"name": "Sync result tracking",
"max_score": 8,
"description": "Tracks and logs counts of updated, skipped, and errored items in the sync result"
}
]
}
Real-Time Inventory Availability Sync
Problem/Feature Description
BrightFrame, an online furniture retailer, sources most of its product catalog from a NetSuite ERP that manages warehouse stock across three fulfillment centers. Their storefront currently shows stale inventory — updated once per night via a full data dump — causing customers to add out-of-stock items to their cart and leading to a high rate of order cancellations. Customer complaints have spiked around popular product lines, and the merchandising team is losing trust in the website's stock indicators.
The engineering team needs a scheduled inventory sync service that runs frequently and keeps e-commerce stock levels closely aligned with ERP data. The service must be efficient enough to run often without hammering the ERP's API — meaning it should only pull records that have actually changed since the last run, handle large catalogs gracefully, and be fast to query at storefront request time.
Output Specification
Implement the inventory sync service in TypeScript. Produce the following files:
src/services/inventorySyncService.ts— The core sync service with asyncInventoryLevels()method that performs the full sync cycle, including paginated ERP fetching, quantity calculation, database updates, and cache updatessrc/scheduler.ts— Scheduler setup that callssyncInventoryLevels()on a recurring basissrc/types.ts— TypeScript interfaces for ERP inventory items, sync results, and related types
Write a short DESIGN.md explaining how the service avoids redundant updates, how it handles large catalogs, and how it keeps the storefront cache fresh.
{
"name": "finsi/erp-integration",
"version": "0.1.0",
"summary": "ERP sync patterns (SAP, NetSuite, Odoo) for orders, inventory, and customers",
"skills": {
"erp-integration": {
"path": "SKILL.md"
}
}
}