How to Connect WooCommerce to Salesforce (Automated Data Sync)
📊 Integration Overview This integration blueprint outlines the technical pipeline for real-time synchronization of order and customer data from WooCommerce to Salesforce. When a new order is placed in WooCommerce, a webhook triggers an event that captures the order and associated customer details. This data is then transformed and mapped to the appropriate Salesforce objects (Account, Contact, Opportunity, OpportunityLineItem), ensuring that sales and customer relationship management systems are consistently updated. The pipeline is designed for robustness, handling data transformations, error conditions, and rate limits to maintain data integrity and availability.
Available integrations in directory: ["shopify-to-freshbooks","shopify-to-hubspot","shopify-to-klaviyo","shopify-to-mailchimp","shopify-to-netsuite","shopify-to-quickbooks","shopify-to-salesforce","shopify-to-waveaccounting","shopify-to-xero","shopify-to-zohocrm","stripe-to-hubspot","woocommerce-to-hubspot","woocommerce-to-quickbooks","woocommerce-to-xero"]. For other e-commerce CRM integrations, consider Shopify to Salesforce or explore our WooCommerce to HubSpot integration. If you're looking for accounting syncs, we also offer WooCommerce to QuickBooks and WooCommerce to Xero.
🛠️ Core Connection Requirements
Primary Key: customer_email (for Salesforce Contact/Account deduplication), order_id (for Salesforce Opportunity deduplication)
Trigger Event: Order created in WooCommerce
Action Event: Create/Update Account, Create/Update Contact, Create/Update Opportunity in Salesforce
📋 The 5-Step Execution Blueprint
Step 1: Authentication & Scope Configuration
Secure authentication to both WooCommerce and Salesforce is paramount. For WooCommerce, REST API keys (Consumer Key and Consumer Secret) with read/write permissions are required. For Salesforce, an OAuth 2.0 Connected App should be configured, granting api, full, refresh_token, and web scopes to enable secure access and token refreshing.
// .env configuration for secure credential storage
// WooCommerce API Credentials
WOOCOMMERCE_CONSUMER_KEY="ck_****************************************"
WOOCOMMERCE_CONSUMER_SECRET="cs_****************************************"
WOOCOMMERCE_WEBHOOK_SECRET="wh_****************************************" // Secret for webhook signature validation
// Salesforce OAuth 2.0 Credentials
SALESFORCE_CLIENT_ID="*************************.<client-id>"
SALESFORCE_CLIENT_SECRET="*************************"
SALESFORCE_USERNAME="your-salesforce-user@example.com"
SALESFORCE_PASSWORD="your-password"
SALESFORCE_SECURITY_TOKEN="*******************" // Appended to password for non-MFA users or programmatic login
SALESFORCE_LOGIN_URL="https://login.salesforce.com" // Or https://test.salesforce.com for sandbox
SALESFORCE_REDIRECT_URI="http://localhost:3000/oauth/callback" // For Authorization Code Flow
Step 2: Webhook Trigger Setup
A webhook listener needs to be set up in WooCommerce to notify our integration when a new order is created. This endpoint must be publicly accessible and capable of validating the incoming request's authenticity using the x-wc-webhook-signature header.
import express from 'express';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const port = process.env.PORT || 3000;
const WOOCOMMERCE_WEBHOOK_SECRET = process.env.WOOCOMMERCE_WEBHOOK_SECRET || '';
// Raw body parser for webhook signature validation
app.use(express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/webhooks/woocommerce/order-created', (req: any, res) => {
const hmac = crypto.createHHmac('sha256', WOOCOMMERCE_WEBHOOK_SECRET);
const signature = req.headers['x-wc-webhook-signature'];
if (!signature) {
console.error('WooCommerce Webhook: Missing signature header.');
return res.status(401).send('Unauthorized: Missing signature.');
}
hmac.update(req.rawBody);
const digest = hmac.digest('base64');
if (signature !== digest) {
console.error('WooCommerce Webhook: Invalid signature.', { expected: digest, received: signature });
return res.status(401).send('Unauthorized: Invalid signature.');
}
console.log('WooCommerce Webhook received and validated.');
const orderData = req.body;
// Process orderData asynchronously (e.g., add to a queue)
console.log('Order Data:', orderData.id);
res.status(200).send('Webhook received successfully.');
});
app.listen(port, () => {
console.log(`Webhook listener running on http://localhost:${port}`);
});
In your WooCommerce admin, navigate to "WooCommerce" -> "Settings" -> "Advanced" -> "Webhooks" and create a new webhook for the "Order created" topic, pointing to your /webhooks/woocommerce/order-created endpoint. Set the Secret field to WOOCOMMERCE_WEBHOOK_SECRET defined in your .env.
Step 3: Payload Transformation & Mapping
Upon receiving a Order created webhook, the raw WooCommerce data must be transformed into a structure compatible with Salesforce objects. This involves mapping WooCommerce customer data to Salesforce Contact and Account, and order data to Salesforce Opportunity and OpportunityLineItem. Custom fields in Salesforce (e.g., WooCommerce_Customer_ID__c, WooCommerce_Order_ID__c) are essential for maintaining external IDs for deduplication.
// Sample WooCommerce Order Payload (Input)
{
"id": 12345,
"parent_id": 0,
"status": "processing",
"currency": "USD",
"date_created": "2023-10-26T10:00:00",
"total": "99.99",
"customer_id": 6789,
"billing": {
"first_name": "John",
"last_name": "Doe",
"address_1": "123 Main St",
"city": "Anytown",
"state": "NY",
"postcode": "12345",
"country": "US",
"email": "john.doe@example.com",
"phone": "555-123-4567"
},
"line_items": [
{
"id": 1,
"name": "Product A",
"product_id": 101,
"quantity": 1,
"total": "49.99"
},
{
"id": 2,
"name": "Product B",
"product_id": 102,
"quantity": 2,
"total": "50.00"
}
]
}
// Mapped Salesforce Payload (Output - for creating/updating) // Contact and Account mapping
{
"Contact": {
"Email": "john.doe@example.com",
"FirstName": "John",
"LastName": "Doe",
"Phone": "555-123-4567",
"MailingStreet": "123 Main St",
"MailingCity": "Anytown",
"MailingState": "NY",
"MailingPostalCode": "12345",
"MailingCountry": "US",
"WooCommerce_Customer_ID__c": "6789" // Custom field for idempotency
},
"Account": { // Often created/updated alongside Contact if not directly tied to a specific company
"Name": "John Doe", // Or derived from company name if available
"BillingStreet": "123 Main St",
"BillingCity": "Anytown",
"BillingState": "NY",
"BillingPostalCode": "12345",
"BillingCountry": "US",
"WooCommerce_Customer_ID__c": "6789" // Custom field for idempotency
},
"Opportunity": {
"Name": "WooCommerce Order #12345",
"Amount": 99.99,
"CloseDate": "2023-10-26", // Or a future estimated close date
"StageName": "Closed Won", // Or appropriate stage for a new order
"WooCommerce_Order_ID__c": "12345", // Custom field for idempotency
"AccountId": "<Salesforce_Account_ID>", // Linked after Account/Contact upsert
"ContactId": "<Salesforce_Contact_ID>" // Linked after Account/Contact upsert
},
"OpportunityLineItems": [
{
"OpportunityId": "<Salesforce_Opportunity_ID>",
"PricebookEntryId": "<Salesforce_Product_PricebookEntry_ID>", // Requires product mapping
"Quantity": 1,
"UnitPrice": 49.99,
"Description": "Product A"
},
{
"OpportunityId": "<Salesforce_Opportunity_ID>",
"PricebookEntryId": "<Salesforce_Product_PricebookEntry_ID>",
"Quantity": 2,
"UnitPrice": 25.00, // original price was 50 for 2, so 25 each
"Description": "Product B"
}
]
}
Step 4: Endpoint Despatch & Error Guarding API requests to Salesforce should be made using a robust client. Common HTTP errors must be handled gracefully with appropriate retry mechanisms and logging.
import { Connection } from 'jsforce';
import dotenv from 'dotenv';
import { Queue } from 'bullmq'; // For rate limiting
dotenv.config();
const salesforceConnection = new Connection({
oauth2: {
clientId: process.env.SALESFORCE_CLIENT_ID || '',
clientSecret: process.env.SALESFORCE_CLIENT_SECRET || '',
redirectUri: process.env.SALESFORCE_REDIRECT_URI || '',
},
loginUrl: process.env.SALESFORCE_LOGIN_URL,
});
// Configure BullMQ queue for Salesforce API calls
const salesforceQueue = new Queue('salesforceApiQueue', {
connection: {
host: 'localhost', // Assuming Redis is running locally
port: 6379,
}
});
async function refreshSalesforceToken(conn: Connection) {
try {
// This assumes an OAuth refresh token flow
// For password flow, you might re-authenticate with username/password
await conn.oauth2.refreshToken(conn.refreshToken as string);
console.log('Salesforce token refreshed successfully.');
} catch (err) {
console.error('Failed to refresh Salesforce token:', err);
throw new Error('Authentication failure: Could not refresh Salesforce token.');
}
}
async function upsertSalesforceRecord(sObject: string, externalIdField: string, externalId: string, data: any, retryCount = 0): Promise<string> {
try {
const result = await salesforceConnection.sobject(sObject).upsert(data, externalIdField);
if (result.success) {
console.log(`Successfully upserted ${sObject} with ID: ${result.id}`);
return result.id;
} else {
throw new Error(`Failed to upsert ${sObject}: ${JSON.stringify(result.errors)}`);
}
} catch (error: any) {
if (error.statusCode === 401 && retryCount < 1) { // Unauthorized - token expired
console.warn('Salesforce 401 Unauthorized. Attempting to refresh token...');
await refreshSalesforceToken(salesforceConnection);
return upsertSalesforceRecord(sObject, externalIdField, externalId, data, retryCount + 1); // Retry once
} else if (error.statusCode === 429) { // Rate limit exceeded
console.warn('Salesforce 429 Rate Limit Exceeded. Adding to queue for retry...');
await salesforceQueue.add('upsertRecord', { sObject, externalIdField, externalId, data }, {
delay: Math.min(Math.pow(2, retryCount) * 1000, 60000) // Exponential backoff, max 1 minute delay
});
throw new Error('Rate limit hit, job queued.');
} else if (error.statusCode === 400) { // Bad Request - validation error
console.error(`Salesforce 400 Bad Request for ${sObject} (externalId: ${externalId}). Payload: ${JSON.stringify(data)}. Errors: ${error.message}`);
throw new Error(`Data validation error for ${sObject}: ${error.message}`);
} else if (error.statusCode >= 500) { // Server error
console.error(`Salesforce ${error.statusCode} Server Error for ${sObject} (externalId: ${externalId}). Retrying...`);
if (retryCount < 3) { // Max 3 retries for server errors
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 2000));
return upsertSalesforceRecord(sObject, externalIdField, externalId, data, retryCount + 1);
}
throw new Error(`Salesforce server error after multiple retries for ${sObject}: ${error.message}`);
} else {
console.error(`Unexpected Salesforce API error for ${sObject} (externalId: ${externalId}): ${error.message}`);
throw error;
}
}
}
// Example usage in an async processing function:
async function processWooCommerceOrder(orderData: any) {
try {
// 1. Authenticate to Salesforce if not already (or refresh token)
if (!salesforceConnection.accessToken || salesforceConnection.isExpired()) {
await salesforceConnection.login(process.env.SALESFORCE_USERNAME || '', process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_SECURITY_TOKEN);
console.log('Logged into Salesforce.');
}
const mappedContact = { /* ... from Step 3 ... */ Email: orderData.billing.email, FirstName: orderData.billing.first_name, LastName: orderData.billing.last_name, WooCommerce_Customer_ID__c: orderData.customer_id.toString() };
const mappedAccount = { /* ... from Step 3 ... */ Name: orderData.billing.first_name + ' ' + orderData.billing.last_name, WooCommerce_Customer_ID__c: orderData.customer_id.toString() };
// 2. Upsert Account (by email or custom ID)
let accountId: string;
try {
// First, attempt to find an existing account by WooCommerce_Customer_ID__c or by the email on related contacts
const existingAccounts = await salesforceConnection.query<{ Id: string }>(
`SELECT Id FROM Account WHERE WooCommerce_Customer_ID__c = '${orderData.customer_id}' LIMIT 1`
);
if (existingAccounts.records.length > 0) {
accountId = existingAccounts.records[0].Id;
console.log(`Found existing Account ID: ${accountId}`);
// Update existing account
await upsertSalesforceRecord('Account', 'Id', accountId, { ...mappedAccount, Id: accountId });
} else {
// If not found by custom ID, try to find a contact by email and link to its account
const existingContacts = await salesforceConnection.query<{ Id: string, AccountId: string }>(
`SELECT Id, AccountId FROM Contact WHERE Email = '${orderData.billing.email}' LIMIT 1`
);
if (existingContacts.records.length > 0 && existingContacts.records[0].AccountId) {
accountId = existingContacts.records[0].AccountId;
console.log(`Found existing Contact by email linked to Account ID: ${accountId}`);
// Update existing account
await upsertSalesforceRecord('Account', 'Id', accountId, { ...mappedAccount, Id: accountId });
} else {
// No existing account or contact with account found, create new account
accountId = await upsertSalesforceRecord('Account', 'WooCommerce_Customer_ID__c', orderData.customer_id.toString(), mappedAccount);
}
}
} catch (err) {
console.error("Error finding/creating Account:", err);
throw err;
}
// 3. Upsert Contact (by email or custom ID, linked to Account)
let contactId: string;
try {
const existingContacts = await salesforceConnection.query<{ Id: string }>(
`SELECT Id FROM Contact WHERE Email = '${orderData.billing.email}' LIMIT 1`
);
if (existingContacts.records.length > 0) {
contactId = existingContacts.records[0].Id;
console.log(`Found existing Contact ID: ${contactId}`);
// Update existing contact
await upsertSalesforceRecord('Contact', 'Id', contactId, { ...mappedContact, Id: contactId, AccountId: accountId });
} else {
contactId = await upsertSalesforceRecord('Contact', 'WooCommerce_Customer_ID__c', orderData.customer_id.toString(), { ...mappedContact, AccountId: accountId });
}
} catch (err) {
console.error("Error finding/creating Contact:", err);
throw err;
}
// 4. Upsert Opportunity (by WooCommerce_Order_ID__c)
const mappedOpportunity = {
Name: `WooCommerce Order #${orderData.id}`,
Amount: parseFloat(orderData.total),
CloseDate: new Date().toISOString().split('T')[0], // Today's date
StageName: 'Closed Won',
WooCommerce_Order_ID__c: orderData.id.toString(),
AccountId: accountId,
ContactId: contactId
};
const opportunityId = await upsertSalesforceRecord('Opportunity', 'WooCommerce_Order_ID__c', orderData.id.toString(), mappedOpportunity);
// 5. Create OpportunityLineItems (requires mapping to Salesforce Products/Pricebook Entries)
for (const item of orderData.line_items) {
// In a real scenario, you'd look up the PricebookEntryId based on product_id/name
// For this example, we'll use a placeholder
const pricebookEntryId = '01t000000000000AAA'; // Placeholder: Replace with actual PricebookEntry ID
const mappedLineItem = {
OpportunityId: opportunityId,
PricebookEntryId: pricebookEntryId,
Quantity: item.quantity,
UnitPrice: parseFloat(item.total) / item.quantity,
Description: item.name
};
// Salesforce Line Items do not have external ID for upsert, typically deleted/recreated or managed by Opportunity
await salesforceConnection.sobject('OpportunityLineItem').create(mappedLineItem);
console.log(`Created OpportunityLineItem for product: ${item.name}`);
}
console.log(`Successfully processed WooCommerce Order ${orderData.id} to Salesforce.`);
} catch (error) {
console.error('Failed to process WooCommerce order to Salesforce:', error);
// Implement alerting for critical failures
}
}
Step 5: Live Loop Validation Thorough testing in a sandbox environment is crucial before deploying to production.
- Environment Setup: Ensure WooCommerce (staging) and Salesforce (sandbox) instances are linked.
- Test Orders: Create multiple types of test orders in WooCommerce (single item, multiple items, new customer, existing customer).
- Data Verification:
- Salesforce UI: Navigate to Salesforce and verify the creation or update of Accounts, Contacts, Opportunities, and Opportunity Line Items. Check that all mapped fields are populated correctly without truncation.
- SOQL Queries: Execute SOQL queries to programmatically verify data integrity.
SELECT Id, Name, WooCommerce_Order_ID__c, Amount, StageName, Account.Name, Contact.Email FROM Opportunity WHERE WooCommerce_Order_ID__c = '12345' SELECT Id, Email, FirstName, LastName, WooCommerce_Customer_ID__c, Account.Name FROM Contact WHERE Email = 'john.doe@example.com' SELECT Id, Name, Quantity, UnitPrice, Opportunity.Name FROM OpportunityLineItem WHERE Opportunity.WooCommerce_Order_ID__c = '12345' - Deduplication: Confirm that creating a new order for an existing customer updates the existing Contact/Account and creates a new Opportunity, rather than creating duplicate customer records or updating an incorrect existing opportunity.
- Error Scenario Testing: Simulate webhook failures, Salesforce API errors (e.g., sending invalid data, temporary network issues) to ensure error guarding and retry mechanisms function as expected.
- Performance Test: For high-volume stores, simulate concurrent orders to validate the queueing and rate-limiting effectiveness.
❓ Integration Frequently Asked Questions
Q: How does this pipeline handle duplicate data entries?
A: The pipeline employs an upsert strategy for Salesforce Account, Contact, and Opportunity records to prevent duplicates. For Account and Contact, we first attempt to find an existing record using the customer's email address or a custom external ID (WooCommerce_Customer_ID__c). If a match is found, the existing record is updated; otherwise, a new one is created. For Opportunity, a unique custom field (WooCommerce_Order_ID__c) is created on the Opportunity object in Salesforce. Before creating a new Opportunity, a SOQL query checks if an Opportunity with that specific WooCommerce_Order_ID__c already exists. If it does, the existing Opportunity is updated, ensuring idempotency for order processing. This prevents the creation of multiple Salesforce records for the same WooCommerce customer or order.
Q: What happens if the API rate limit is exceeded during high volume? A: To gracefully handle Salesforce API rate limits during peak periods, the integration implements an asynchronous queuing mechanism, such as using Redis with BullMQ. When an API call to Salesforce returns a 429 (Too Many Requests) HTTP status code, the failing API request is not immediately reattempted. Instead, it is pushed onto a dedicated queue. The queue worker then processes these jobs at a controlled rate, typically with an exponential backoff strategy, introducing increasing delays between retries to avoid re-triggering the rate limit. This buffering ensures that all orders are eventually processed without data loss, while maintaining the stability and compliance with Salesforce's API governance limits. Administrators are notified if the queue backlog becomes excessively large, indicating a sustained high volume or potential issue requiring intervention.