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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window