Integration API
/api/integration — how the ERP pushes data in
How the ERP's data gets into Commerce. The ERP pushes; Commerce does not pull. This is the surface that makes the platform ERP-neutral on the way in.
Who calls it
The ERP, or the middleware in front of it. Today that is Business Central — but nothing on this surface knows that, which is the point.
| Authentication | Bearer JWT, on a scheme of its own |
| Authorization | InboundPolicy, on that scheme only |
| Scope | Bypass. A batch covers many customers |
| Contract stability | Published. Upstream depends on it |
One envelope for every write
Every endpoint here takes the same shape, so an upstream integrator implements one contract rather than seven.
{
"source": "BC",
"timestamp": "2026-08-19T04:00:00Z",
"data": [
{
"sku": "SHIRT-BLK-M",
"description": "Classic Shirt Black M"
},
{
"sku": "SHIRT-BLK-L",
"description": "Classic Shirt Black L"
}
]
}
| Field | Meaning |
|---|---|
source |
Required. Which upstream system sent this |
timestamp |
Optional, informational — the sender's send time |
data |
Required. A batch. Each item carries its own keys and is processed individually |
And every write answers in the same shape — one result per item, keyed by that item's natural identifier, with optional nested results where one item produced several entities:
{
"data": [
{
"key": "C00123",
"outcome": "Upserted",
"type": "Account",
"children": [
{ "key": "SHIP-1", "outcome": "Upserted" }
]
}
]
}
Why per-item results rather than one status for the batch. A batch of five hundred products where three are malformed should not fail wholesale, and the sender needs to know which three. Reporting per item makes a partial success expressible instead of forcing an all-or-nothing answer.
Note
outcome is currently always "Upserted" — the services underneath do not report whether they
inserted or updated. The field exists so that "Created" and "Updated" can be distinguished later
without changing the shape.
Every endpoint
Twelve, grouped by the controller that serves them. Each group is one domain.
| Group | Domain | Endpoints |
|---|---|---|
| Catalogue | Catalogue | PUT /catalogue/products |
| Customer | Customer | PUT /customer · PUT /customer/ledger |
| Fulfilment | Fulfilment | PUT /fulfilment/postcode-routing · /freight-rates · /airbags |
| Inventory | Inventory | PUT /inventory |
| Pricing | Pricing | PUT /pricing/rules |
| Tax | Tax | PUT /tax/jurisdictions · /areas · /gst-posting-rates |
| Trading | Trading | PUT /trading/order-line-updates |
All are PUT — every one is an upsert, and re-sending the same payload is safe.
Trading is the only one that flows back: once an order has been posted to the ERP, the ERP reports what was invoiced, shipped or cancelled per line. Order status in Commerce is derived from those quantities rather than stored, so a repeated update changes nothing.
Snapshot or not — the difference that bites
Most endpoints treat a payload as a snapshot: a child row stored in Commerce but absent from the payload is deleted. Omission is deletion, and there is no per-row delete signal.
| Snapshot — omission deletes | Customer ship-to addresses · Inventory locations · Tax jurisdiction rates and area links |
| Not a snapshot — omission is ignored | Pricing rules, which are only ever inserted or overwritten by ruleId |
The snapshot model is deliberate: upstream systems rarely emit reliable delete events for child rows, so reconciling from a snapshot moves that burden off the integrator. But Pricing does not work that way, and an integration that assumes it does will accumulate stale price rules indefinitely.
Two rules that are easy to get wrong
Audit timestamps are not part of the contract. Inbound payloads do not declare when a record was created or modified, and a value sent is ignored. Those columns record when Commerce learned something, which is a question only Commerce can answer. See Database.
Correlation travels on a header, not in the envelope. Send X-Correlation-Id and it is echoed on
the response, so you can tie your own logs to a Commerce request. Note that Commerce does not currently
carry it into the background work a push triggers — see Logging.
Why this surface is the ERP-neutral half
Commerce publishes a contract and waits. It does not know what wrote the payload, does not hold an ERP client to receive it, and does not care whether the sender is an ERP, a middleware bus or a script. Any system able to speak the envelope can feed the platform.
The outbound direction is not yet symmetrical — placing an order calls Business Central through a typed client, so a second ERP needs an adapter there. The asymmetry is deliberate to acknowledge rather than paper over: inbound is done, outbound is not.