Microsoft Dynamics 365 Business Central is the ERP of choice for many mid-market companies, especially those already invested in the Microsoft ecosystem. Integrating it with WooCommerce is not, in my experience, a hard engineering problem. It is a decision problem: pick the wrong integration pattern for your scale and you will spend the next two years maintaining plumbing instead of running a store. So before any code, understand D365’s API landscape and choose the pattern that matches your volume and complexity — not the one with the best marketing.
D365 Business Central API Options
OData/REST APIs (Standard)
Business Central exposes standard entities via OData v4 endpoints:
GET https://{tenant}.api.businesscentral.dynamics.com/v2.0/{environment}/api/v2.0/items
GET https://{tenant}.api.businesscentral.dynamics.com/v2.0/{environment}/api/v2.0/salesOrders
GET https://{tenant}.api.businesscentral.dynamics.com/v2.0/{environment}/api/v2.0/customers
Authentication: OAuth 2.0 via Azure AD. Register an app in Azure Portal, grant Financials.ReadWrite.All permissions.
Custom APIs (AL Extensions)
For complex integrations, build custom API endpoints in Business Central using AL language:
page 50100 "WooCommerce Items API"
{
PageType = API;
APIPublisher = 'wpclan';
APIGroup = 'woocommerce';
APIVersion = 'v1.0';
EntityName = 'item';
EntitySetName = 'items';
SourceTable = Item;
// ... field definitions
}
My rule of thumb: reach for a custom AL API only when a standard endpoint genuinely cannot express what you need — a projection across tables, or a field the standard entity does not surface. Every custom API page is code that someone on the Business Central side now owns forever. Standard endpoints are maintained by Microsoft; your custom page is maintained by you.
Dataverse (Advanced)
For bi-directional sync with change tracking, Business Central can sync data to Dataverse (formerly Common Data Service). This provides webhook-like change notifications.
Integration Patterns
Pattern 1: Power Automate (Low-Code)
Best for: Simple integrations, < 100 orders/day
Microsoft Power Automate can connect D365 to WooCommerce via HTTP connectors:
Trigger: When a WooCommerce order is created (HTTP webhook)
→ Parse JSON (order data)
→ D365: Find or create customer
→ D365: Create sales order
→ D365: Add line items
→ WooCommerce: Update order meta with D365 reference
Pros: No code, visual workflow, built-in error handling
Cons: API call limits (Performance plan needed), slower execution, limited transformation logic
Pattern 2: Azure Functions + Service Bus (Cloud-Native)
Best for: Medium-high volume, Microsoft-stack teams
WooCommerce Webhook → Azure Function → Service Bus Queue → Azure Function → D365 API
D365 Change Feed → Azure Function → Service Bus Queue → Azure Function → WooCommerce API
Azure Functions handle the API calls and data transformation. Service Bus provides reliable message queuing with dead-letter support.
Pattern 3: Custom Node.js Middleware (Full Control)
Best for: Complex requirements, custom business logic
// D365 Business Central client
class D365Client {
constructor(tenantId, clientId, clientSecret, environment) {
this.baseUrl = https://api.businesscentral.dynamics.com/v2.0/${environment}/api/v2.0;
this.tokenUrl = https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
async getToken() {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
scope: 'https://api.businesscentral.dynamics.com/.default'
})
});
const data = await response.json();
return data.access_token;
}
async getItems() {
const token = await this.getToken();
const response = await fetch(${this.baseUrl}/items, {
headers: { 'Authorization': Bearer ${token} }
});
return response.json();
}
async createSalesOrder(orderData) {
const token = await this.getToken();
return fetch(${this.baseUrl}/salesOrders, {
method: 'POST',
headers: {
'Authorization': Bearer ${token},
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
});
}
}
Which pattern I would actually choose
Here is how I decide, and I decide before writing a line of integration code. Count the orders per day, honestly, at the seasonal peak rather than the quiet Tuesday average. Under 100 orders a day and no exotic transformation logic, I start with Power Automate every time — the visual workflow means the client’s own IT can read it, and “someone other than the original developer can understand this” is worth more over five years than any elegance. Do not talk yourself into custom middleware for a shop doing forty orders a day because it feels more professional. It is not more professional. It is more expensive to keep alive.
The moment the daily volume climbs into the hundreds, or reliability becomes contractual — where a dropped order is a real financial incident, not an annoyance — I move to Pattern 2. The reason is the Service Bus queue, not the Azure Functions. A queue with dead-letter support is what lets a failed D365 write sit safely and retry instead of vanishing. That single property is the difference between “we lost three orders during the outage” and “everything caught up when D365 came back.” I reserve Pattern 3, the full Node.js middleware, for the genuinely irreducible case: bespoke business logic that neither a low-code flow nor a stock function app can express cleanly. It is the most powerful option and the one you are most likely to regret building if a simpler pattern would have done the job.
Data Mapping: D365 → WooCommerce
Items to Products
| D365 Field | WooCommerce | Notes |
|---|---|---|
| number | sku | Primary key for mapping |
| displayName | name | |
| unitPrice | regular_price | From price list |
| inventory | stock_quantity | Real-time critical |
| itemCategoryCode | categories | Map codes to WC category IDs |
| blocked | status | blocked=true → draft |
| gtin | meta: _gtin | For schema markup |
Sales Orders
| WooCommerce | D365 Sales Order | Notes |
|---|---|---|
| order.id | externalDocumentNumber | Cross-reference |
| billing.email | customerNumber | Match by email |
| line_items[].sku | salesOrderLines[].itemId | Match by item number |
| line_items[].quantity | salesOrderLines[].quantity | |
| line_items[].total | salesOrderLines[].unitPrice | Recalculate for discounts |
| shipping_total | — | Add as separate line or freight charge |
The one row on these tables I would build first, and test hardest, is the SKU-to-number mapping. It is the primary key the whole integration hangs on. If a product exists in WooCommerce with a SKU that does not exactly match a D365 item number — a trailing space, a leading zero stripped by a spreadsheet, a manually created product — every order containing it will fail in a way that looks mysterious at 2am. Reconcile the two catalogues on SKU before you turn on order sync, not after.
Handling D365’s Pagination
Business Central APIs return max 20,000 records and use @odata.nextLink for pagination:
async function getAllItems(client) {
let items = [];
let url = ${client.baseUrl}/items?$top=1000;
while (url) {
const response = await client.authenticatedFetch(url);
const data = await response.json();
items = items.concat(data.value);
url = data['@odata.nextLink'] || null;
}
return items;
}
Inventory Sync Strategy
D365 Business Central tracks inventory at the location (warehouse) level. Your sync must decide:
- Sum all locations for a single WooCommerce stock count?
- Sync specific locations (e.g., only the ecommerce warehouse)?
- Reserve buffer stock (show 90% of actual stock to avoid overselling)?
async function syncInventory() {
const d365Items = await d365.getItems();
const updates = d365Items.value
.filter(item => !item.blocked)
.map(item => ({
sku: item.number,
stock_quantity: Math.floor(item.inventory * 0.95), // 5% buffer
stock_status: item.inventory > 0 ? 'instock' : 'outofstock'
}));
// Batch update WooCommerce
for (let i = 0; i < updates.length; i += 100) {
await wooApi.post('products/batch', {
update: updates.slice(i, i + 100).map(u => ({
sku: u.sku, // requires SKU lookup to get product ID
stock_quantity: u.stock_quantity,
stock_status: u.stock_status
}))
});
}
}
Of the three choices above, I default to syncing a single named ecommerce location with a buffer, not the sum of every warehouse. Summing all locations is how you promise stock that is physically sitting in a warehouse the online store is not allowed to ship from. The buffer is not paranoia; it is the margin that absorbs the lag between an in-store sale in D365 and the next inventory push. Overselling costs you a refund, an apology, and a customer. A slightly conservative stock count costs you almost nothing.
Common D365 Integration Pitfalls
- OAuth token expiration: Tokens expire after 1 hour. Always refresh before API calls.
- Rate limiting: Business Central has a 600 calls/minute limit per tenant. Batch your requests.
- Number sequences: D365 auto-generates document numbers. Don’t try to set them manually.
- Dimensions: D365’s financial dimensions (cost center, department) have no WooCommerce equivalent. Plan how to handle them.
- Multi-currency: D365 handles multi-currency natively. WooCommerce needs a plugin for multi-currency. Map exchange rates carefully.
The failure mode I would plan for first
Every list of pitfalls reads as equally weighted. It is not. The one that will actually take your store down is the OAuth token expiry, precisely because it works flawlessly in testing and then breaks in production an hour after your first successful sync. You test, it works, you ship, everyone goes home. Ninety minutes later the token your integration cached has died and every write starts failing silently. Build the token refresh — with a small clock-skew margin so you renew before expiry, not exactly at it — as the very first thing, before any business logic. Then rehearse the reconnect: kill D365 access on a staging tenant, watch your integration fail, bring it back, and confirm the queue drains and nothing was lost. An integration that has never been watched failing is an integration you do not yet understand.
Conclusion
D365 Business Central + WooCommerce is a strong combination. Power Automate works for simple, low-volume integrations. Azure Functions suit Microsoft-stack teams needing reliability. Custom middleware gives you full control for complex scenarios. Regardless of the pattern, the fundamentals are the same: map your data carefully, handle inventory as the highest-priority sync, and build robust error handling from day one. Choose the smallest pattern that fits your volume, and let the store earn its way up to the next one.
Last modified: August 2, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe