Off-the-shelf ERP connectors cover 70% of use cases. The other 30% — custom fields, complex pricing rules, multi-warehouse logic, approval workflows — require a custom connector. Here’s how to build one that’s reliable, maintainable, and production-ready.
Architecture
A custom connector is a middleware service that sits between WooCommerce and your ERP:
┌─────────────┐ ┌──────────────────┐ ┌──────────┐
│ WooCommerce │────>│ Middleware │────>│ ERP │
│ REST API │<────│ (Node.js/Python) │<────│ API │
└─────────────┘ │ │ └──────────┘
│ ┌────────────┐ │
│ │ Queue │ │
│ │ (Redis) │ │
│ └────────────┘ │
│ ┌────────────┐ │
│ │ DB │ │
│ │ (mapping) │ │
│ └────────────┘ │
└──────────────────┘
The Sync Engine
The core of your connector is a sync engine that processes data in both directions:
class SyncEngine {
constructor(wooClient, erpClient, db) {
this.woo = wooClient;
this.erp = erpClient;
this.db = db;
this.queue = new Queue('sync-jobs');
}
// Product sync: ERP → WooCommerce
async syncProducts() {
const lastSync = await this.db.getLastSync('products');
const erpProducts = await this.erp.getModifiedProducts(lastSync);
for (const product of erpProducts) {
await this.queue.add('sync-product', {
erpId: product.id,
data: product
});
}
}
// Order sync: WooCommerce → ERP
async syncOrders() {
const lastSync = await this.db.getLastSync('orders');
const wcOrders = await this.woo.getOrdersSince(lastSync);
for (const order of wcOrders) {
await this.queue.add('sync-order', {
wcOrderId: order.id,
data: order
});
}
}
}
Data Mapping Layer
The most critical component. Create a mapping configuration that’s separate from your sync logic:
// mapping.js
const PRODUCT_MAPPING = {
// ERP field → WooCommerce field
'item_code': 'sku',
'item_name': 'name',
'description': { field: 'description', transform: 'htmlEncode' },
'unit_price': { field: 'regular_price', transform: 'toString' },
'stock_qty': { field: 'stock_quantity', transform: 'parseInt' },
'is_active': { field: 'status', transform: (val) => val ? 'publish' : 'draft' },
'weight_kg': { field: 'weight', transform: 'toString' },
'category_code': { field: 'categories', transform: 'mapCategory' }
};
const ORDER_MAPPING = {
// WooCommerce field → ERP field
'id': 'external_ref',
'billing.email': 'customer_email',
'billing.first_name': { field: 'customer_name', transform: 'combineName' },
'line_items': { field: 'order_lines', transform: 'mapLineItems' },
'total': { field: 'order_total', transform: 'parseFloat' }
};
function applyMapping(source, mapping) {
const result = {};
for (const [sourceKey, target] of Object.entries(mapping)) {
const value = getNestedValue(source, sourceKey);
if (typeof target === 'string') {
result[target] = value;
} else {
const transformed = transforms[target.transform]
? transforms[target.transform](value, source)
: value;
result[target.field] = transformed;
}
}
return result;
}
Queue-Based Processing
Never process syncs inline. Use a job queue for reliability:
const Queue = require('bull');
const syncQueue = new Queue('erp-sync', {
redis: { host: '127.0.0.1', port: 6379 },
defaultJobOptions: {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 100,
removeOnFail: 500
}
});
// Process product sync jobs
syncQueue.process('sync-product', async (job) => {
const { erpId, data } = job.data;
// Check if product exists in WooCommerce
const mapping = await db.getMapping('product', erpId);
if (mapping) {
// Update existing product
await wooClient.updateProduct(mapping.wc_id, applyMapping(data, PRODUCT_MAPPING));
} else {
// Create new product
const wcProduct = await wooClient.createProduct(applyMapping(data, PRODUCT_MAPPING));
await db.createMapping('product', erpId, wcProduct.id);
}
return { success: true, erpId };
});
// Handle failures
syncQueue.on('failed', (job, err) => {
console.error(Job ${job.id} failed: ${err.message});
if (job.attemptsMade >= job.opts.attempts) {
alertTeam(Sync failed permanently: ${job.data.erpId} - ${err.message});
}
});
ID Mapping Table
WooCommerce IDs and ERP IDs are different. Maintain a mapping table:
CREATE TABLE entity_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
entity_type ENUM('product', 'customer', 'order', 'category') NOT NULL,
erp_id VARCHAR(100) NOT NULL,
wc_id INT NOT NULL,
last_synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sync_hash VARCHAR(64), -- Hash of data to detect changes
UNIQUE KEY (entity_type, erp_id),
INDEX (entity_type, wc_id)
);
The sync_hash field prevents unnecessary updates. If the ERP data hasn’t changed since the last sync, skip the WooCommerce API call.
function hasChanged(currentData, storedHash) {
const currentHash = crypto.createHash('md5')
.update(JSON.stringify(currentData))
.digest('hex');
return currentHash !== storedHash;
}
Webhook Receiver
For real-time order sync, receive WooCommerce webhooks:
app.post('/webhooks/woocommerce/order-created', (req, res) => {
// Verify webhook signature
const signature = req.headers['x-wc-webhook-signature'];
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('base64');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
// Queue the order for processing
syncQueue.add('sync-order', {
wcOrderId: req.body.id,
data: req.body
});
res.status(200).send('OK');
});
Monitoring Dashboard
Build a simple dashboard to track sync health:
app.get('/api/sync/status', async (req, res) => {
const [productCount] = await db.query('SELECT COUNT(*) as count FROM entity_mapping WHERE entity_type = "product"');
const [orderCount] = await db.query('SELECT COUNT(*) as count FROM entity_mapping WHERE entity_type = "order"');
const [recentFails] = await db.query('SELECT * FROM sync_log WHERE status = "failed" AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)');
const queueStats = await syncQueue.getJobCounts();
res.json({
mappedProducts: productCount[0].count,
mappedOrders: orderCount[0].count,
queue: queueStats,
recentFailures: recentFails.length,
lastProductSync: await db.getLastSync('products'),
lastOrderSync: await db.getLastSync('orders')
});
});
Deployment
Deploy your connector as a standalone service:
- Docker for consistent environments
- PM2 or systemd for process management
- Health check endpoint for uptime monitoring
- Log aggregation via Winston + CloudWatch or ELK
Conclusion
A custom WooCommerce-ERP connector is a significant engineering investment, but it pays off when your integration requirements exceed what off-the-shelf solutions offer. The key components — data mapping, queue processing, ID mapping, error handling, and monitoring — are universal regardless of which ERP you’re connecting to. Build them well once, and you have a foundation that can adapt to changing business requirements.
Last modified: April 3, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe