Export placed Orders to an order management system, and apply status, fulfillment, shipment, cancellation, return, and Inventory updates back to commercetools.
An order management system (OMS) routes, allocates, and fulfills Orders after checkout. This guide covers the two one-way flows that connect it to commercetools: an outbound export of placed Orders, and an inbound flow that applies post-order changes to the Order and to Inventory. It also covers the scheduled reconciliation that repairs whatever those two flows miss.
Define OMS integration requirements
The Connector choice and the sync design are both downstream of these decisions. Record them before you configure or build anything.
Assign a source of truth per data domain
Assign exactly one owner per domain, and make the other side read-only for it. Never synchronize the same field in both directions: a field with two writers produces conflicts, update loops, and lost changes that are difficult to diagnose after the fact.
| Data domain | Typical owner after checkout | Direction |
|---|---|---|
| Order capture and contents | commercetools | commercetools to OMS |
| Order status and workflow | OMS | OMS to commercetools |
| Fulfillment and Line Item status | OMS | OMS to commercetools |
| Shipment, Delivery, and tracking | OMS | OMS to commercetools |
| Cancellations after handoff | OMS | OMS to commercetools |
| Returns and return status | OMS | OMS to commercetools |
| Inventory quantities | The system of record for Orders | Usually OMS to commercetools |
| Customer profile | Usually commercetools | commercetools to OMS |
Scope the export and inbound flows
Answer and record the following questions because each one changes the design:
- Which Orders export, and when? Every Order at creation, or only after payment or approval? An export that must wait for payment subscribes to the state change instead of Order creation, or checks the payment state inside the handler.
- What comes back inbound? Order status, Line Item fulfillment status, shipment data including tracking, cancellations, returns, Inventory levels, or a subset.
- How does the OMS deliver updates? A webhook it calls, an endpoint you poll, or a batch file. A webhook maps to a
serviceapplication. Polling and batch files map to ajobapplication. - What latency and volume must the integration support? Real-time delivery, near-real-time, or an overnight batch. Order and Inventory volume determine whether a single reconciliation run can complete inside its time budget.
- How do identifiers map? How OMS statuses map onto commercetools state, how SKUs map to Inventory locations and supply Channels, and where the OMS identifier is stored on the Order.
- How much drift and delay is tolerable? This sets the reconciliation schedule and the alerting thresholds.
- Does split or partial fulfillment apply? Split shipments, partial fulfillment, and pickup in store each add Deliveries and states that the mapping must cover.
Record special requirements
The list above covers the common shape, not every implementation. Capture additional constraints as separate requirements. Each one can move the decision in the next section from configuring a Connector toward forking or building one:
- B2B flows, including purchase order numbers, approval steps, and Business Unit ownership.
- Marketplace or multi-vendor fulfillment split across sellers. The OMS owns Delivery, Parcel, and tracking; a marketplace integration forwards that status to each seller and must not write it. See Integrate a marketplace service.
- Returns and return merchandise authorization workflows owned by a separate system.
- Recurring Orders and subscription flows.
- Multi-Store, multi-market, or multi-currency routing.
- An existing OMS contract, account, or tenant that constrains the integration shape.
- Custom Order workflows modeled as States.
- Data residency and compliance constraints that restrict the deployment Region.
Choose an integration path
Work through these options in order and stop at the first one that meets your requirements. Each later option is more to build and more to maintain.
Check for an existing OMS Connector
view_connectors scope to call the Search Connectors endpoint and filter by integration type:GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=oms&integrationTypes=shipping
oms and shipping to find relevant candidates. The IntegrationType enum has no separate fulfillment value. For the host to use in each Region, see Hosts and authorization. The same search is available in the Merchant Center.Compare each candidate against your requirements rather than against its headline. Check the flows it implements, its direction and ownership model, available mapping configuration, split shipment and return handling, and support for your Region and volume.
Prove that a gap is not configuration
When a Connector matches your OMS but appears to be missing a behavior, confirm that configuration cannot close the gap before you consider forking it. Inspect message selection, field mappings, state mappings, and enabled flows before you decide that implementation work is required.
Fork or build
If a Connector matches your OMS and has a genuine gap that configuration cannot close, and its source is available, fork it. Add only the difference and deploy the result as an Organization Connector. You keep the working sync scaffolding and maintain a smaller change.
fulfilment-integration template that scaffolds the application structure for these flows:commercetools connect init my-oms-connector --template fulfilment-integration
order-export event application, an order-updates service application, an inventory-import service application, and a product-export event application. Keep the applications your requirements call for and remove the rest. If you need scheduled reconciliation, add a job application, which the template does not include.fulfilment-integration template supplies application structure, not a working OMS integration. This assessment covers commit 30ce085, inspected on 28 August 2026. Before production use, implement the following:- The OMS calls themselves. The export handlers map an Order into a local object, log it, and return a success status. No OMS request exists.
- The
scriptsblock inconnect.yaml. ThepostDeployandpreUndeployfiles exist in each application, but they are not declared, so a deployment creates no Subscription. - Authentication on the inbound
order-updatesandinventory-importendpoints. Both accept unauthenticated requests that write to your Project. - Order status, Line Item state, cancellation, and return handling. The inbound application applies only Add Delivery and Change ShipmentState.
- Idempotency on both flows. The export has no external side effect in the template, while a repeated inbound Order webhook adds a second Delivery.
- Least-privilege permissions. The template expects a pre-created API Client rather than declaring
inheritAs.apiClient.scopes. - Meaningful tests. The suite asserts response codes for malformed requests only.
Whichever option you choose, the sync design in the next section is the same. Only the question of who implements it changes.
Design the sync flows
event application for the export, a service application for inbound updates, and an optional job application for reconciliation.Export placed Orders
event application, which receives Messages through a Subscription and a Connect-provisioned message broker.Subscribe to every Message that represents an exported Order
Retrieve the Order instead of reading the Message payload
resource.id. Subscriptions deliver at least once with no ordering guarantee, so a payload can describe an older state than the Order currently has. A payload that exceeds the queue size limit is omitted entirely and the notification arrives with payloadNotIncluded, which makes your largest Orders the ones that fail. Retrieving the Order removes both problems.Make the export idempotent without local state
orderNumber as the OMS external reference and let the OMS upsert on it, or query the OMS for an existing record before creating one. Connect applications are stateless and autoscaled, so an in-process set of processed identifiers is lost on restart and is not shared between instances. The orderNumber field is optional on an Order and unique across a Project, so confirm that your checkout sets it before you use it as the correlation key.Store the OMS identifier on the Order
externalId field. Use SyncInfo, which exists to record synchronization activity such as an export, and write it with the Update SyncInfo update action. The action requires a Channel whose roles include OrderExport or OrderImport. Without that role the action fails with an InvalidInput error after the OMS has already created the Order, so create the Channel before the first export runs. Read the value back with the syncInfo(externalId="...") Query Predicate. A Custom Field is a valid alternative when you need to store more than an identifier and a Channel.Acknowledge deliberately
102, 200, 201, 202, and 204 as an acknowledgment and retries every other response. Unacknowledged messages are retained for seven days, and push backoff applies when an application returns too many negative acknowledgments, so a sustained OMS outage slows delivery for every Message on the queue. Subscription delivery is retried for up to 48 hours on a TemporaryError, and for a shorter window on a ConfigurationError. Size your reconciliation schedule against those windows rather than against the queue alone.Map each outcome to a status code on purpose:
| Outcome | Response | Reason |
|---|---|---|
| Exported successfully | 200, 202, or 204 | Nothing to retry |
| Message type you do not handle | 200 | Redelivery cannot help |
| Malformed envelope that can never be decoded | 200, with a log entry and an alert | Retrying cannot make the Message valid |
| Transient OMS or network failure | A status outside the acknowledgment set, or none | Redelivery is the recovery path |
| Order that permanently cannot be exported | 200, with a record for replay | An endless retry loop is avoided |
200 for every error is the opposite failure: a transient OMS outage is acknowledged, the Message is discarded, and the Order silently never reaches the OMS.Do not export your own writes back to the OMS
Apply inbound updates
service application as an authenticated webhook. In this mode, no API Extension is registered, the default two-second API Extension response limit does not apply, and the five-minute Connect service timeout applies instead. You authenticate the caller and write to commercetools yourself.Do not implement inbound updates as an API Extension. An API Extension runs synchronously inside a commercetools API request, so OMS latency or an outage would delay or block Cart and Order operations.
Authenticate every request and validate the payload
The endpoint is reachable from the internet and writes to your Project. Verify a shared secret or a fully validated token, including signature, issuer, audience, and expiry, on every processing route. Leave only a liveness route unauthenticated. Keep the webhook secret in secured configuration.
Correlate by the stored identifier
syncInfo(externalId="...") predicate or the equivalent Custom Field predicate. The API does not enforce uniqueness for SyncInfo.externalId, so reject zero or multiple matches and enforce uniqueness in the integration. Never infer the Order from position, sequence, or timing.Read the Order before writing to it
shippingInfo for a single shipping mode, or shipping for multiple shipping modes. In Multiple Shipping Mode, the action also requires shippingKey. A shipment update on an otherwise valid Order fails when the required shipping data is missing.Guard the transition instead of applying it unconditionally
409 version conflict by re-reading the Order and re-evaluating the decision. Re-sending the same actions after a conflict is how a single shipment becomes two Deliveries with duplicate tracking numbers.The following handler applies a shipment update using that sequence: correlate, re-read, decide, write, and re-evaluate on conflict. Key the Delivery on the OMS shipment identifier so a redelivery finds the existing record instead of adding a second one.
async function applyShipmentUpdate(
apiRoot,
omsOrderId: string,
omsShipmentId: string,
trackingId: string
) {
const MAX_ATTEMPTS = 3;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
// Correlate on the identifier stored at export time
const matches = await apiRoot
.orders()
.get({
queryArgs: {
where: `syncInfo(externalId="${omsOrderId}")`,
limit: 2,
},
})
.execute();
// SyncInfo.externalId is not unique by the API, so enforce it here
if (matches.body.results.length !== 1) {
throw new Error(
`Expected exactly one Order for OMS identifier ${omsOrderId}`
);
}
const order = matches.body.results[0];
// A redelivered notification is a no-op
const alreadyApplied = order.shippingInfo?.deliveries?.some(
(delivery) => delivery.key === omsShipmentId
);
if (alreadyApplied) {
return order;
}
try {
const updated = await apiRoot
.orders()
.withId({ ID: order.id })
.post({
body: {
version: order.version,
actions: [
{
action: "addDelivery",
deliveryKey: omsShipmentId,
parcels: [
{ trackingData: { trackingId, isReturn: false } },
],
},
{
action: "changeShipmentState",
shipmentState: "Shipped",
},
],
},
})
.execute();
return updated.body;
} catch (error) {
// Re-read and re-evaluate; never replay these actions against a new version
if (error.statusCode === 409) continue;
throw error;
}
}
throw new Error(`Version conflict persisted for OMS identifier ${omsOrderId}`);
}
Multiple ShippingMode, read the Deliveries from shipping[].shippingInfo.deliveries and pass the matching shippingKey in the Add Delivery action.Return a status the OMS can act on
Use a success status when the update is applied or intentionally skipped, a client error for a payload that will never be valid, and a server error for a transient failure so the OMS retries. Neither the export nor the inbound flow sits on the checkout path, so a transient OMS or commercetools failure should reject the update and retry rather than drop it.
Reconcile drift
job application repairs what the event and webhook paths miss: a dropped webhook, a Message that exhausted its retries, an Order that failed to export, and Inventory that has diverged. Add one whenever your business tolerance for drift is lower than the guarantees of at-least-once delivery.Every unit of work must be idempotent, because reconciliation deliberately reprocesses records that the event path may have already handled. Share one handler between the event path and the reconciliation path rather than writing a second implementation that can drift from the first.
Map OMS statuses to commercetools state
Cancelled, while ShipmentState uses Canceled. Use the type definitions as the canonical value lists.Use the following table as a starting point for an explicit OMS mapping:
| OMS status | commercetools target | Update actions |
|---|---|---|
| Received | Custom Order State Received | Transition State |
| Allocated or picking | Line Item State | Transition LineItem State |
| Shipped, with tracking | shipmentState Shipped, plus a Delivery and Parcel | Change ShipmentState, Add Delivery, Add Parcel to Delivery, Set Parcel Tracking Data |
| Partially shipped | shipmentState Partial, plus one Delivery per shipment | Change ShipmentState, Add Delivery |
| Delivered | orderState Complete | Change OrderState |
| Cancelled | orderState Cancelled | Change OrderState |
| Return initiated or received | Return Item with Advised or Returned state | Add ReturnInfo |
| Returned item assessed | Return shipment state BackInStock or Unusable | Set ReturnShipmentState |
orderState values, model them as a custom State machine and register the States and their transitions idempotently during deployment.Synchronize Inventory
Inventory synchronization is a separate flow with its own direction, cadence, and failure policy. Handle it as its own integration rather than as a side effect of Order updates, so you can tune it independently.
TrackOnly and ReserveOnOrder deduct stock when the Order is created, and ReserveOnCart reserves stock as soon as a Line Item enters the Cart. Only None leaves Inventory untouched. When the OMS owns Inventory and also decrements it on the same Order, any mode other than None deducts the same units twice and produces drift that reconciliation reports forever. Either set the mode to None, or subtract the platform-side deduction in your mapping. See Inventory modes for the full behavior of each mode.Two behaviors shape reconciliation:
- Account for Inventory consistency. Order-driven and reservation-driven changes are eventually consistent, while direct Inventory API updates are immediately visible. Trail reconciliation behind the documented consistency window so temporary lag is not reported as drift. See Inventory checks and consistency.
- Decide which system restocks canceled Inventory. Changing
orderStatetoCancelledrecords the business outcome for the Order, and commercetools documents no automatic restock for it. Require the Inventory owner to send an explicit restock update.
Run a periodic full snapshot in addition to the incremental updates. Incremental Inventory updates can be missed, and the drift is invisible until stock is oversold.
Configure and deploy the Connector
Configure the applications, permissions, and Subscription as one contract.
Grant only the permissions the enabled flows use
inheritAs.apiClient.scopes so Connect generates a scoped API Client at installation, instead of asking an installer to supply credentials. The API Client that creates the Deployment requires manage_api_clients. The generated API Client needs permissions for the enabled Order, Inventory, Message, and registration flows. Inventory writes use manage_products. Message reconciliation requires view_messages, must be enabled for the Project, and can only reach back as far as the Project's Message retention window, which defaults to 15 days. If postDeploy creates resources, add manage_subscriptions, manage_channels, manage_types, and manage_states as applicable. See Modify a Connector and the canonical API scopes.Separate configuration from secrets
connect.yaml configuration rules. Never place OMS credentials or webhook secrets in code or logs.Build the Subscription destination from the injected variables
Register resources idempotently
postDeploy, and remove them in preUndeploy. Declare both in the scripts block of connect.yaml; a script file that is not declared never runs. Prefer creating the Subscription when absent and updating it in place, rather than deleting and recreating it, because the gap between the two operations drops the Messages emitted during it.Verify the registration rather than inferring it from the Deployment status
Validate the OMS connection at deploy time
postDeploy make a test call to the OMS so bad credentials surface immediately instead of on the first Order. Decide whether that failure fails the deployment or only warns, and record the decision.Verify the round trip
Add automated tests before you deploy. Mock both the OMS API and the commercetools API, and assert which actions the code takes:
- Each supported Message type routes to the intended handler, and unhandled types are acknowledged without an OMS call.
- The export retrieves the Order by identifier and handles an omitted payload.
- A redelivered export Message produces one OMS record, not two.
- Each status code in the acknowledgment table is returned for the outcome it represents.
- The self-change filter prevents an inbound status write from re-triggering the export.
- The inbound endpoint rejects unauthenticated and malformed requests.
- A redelivered inbound update is a no-op, and a stale update does not overwrite a newer state.
- A version conflict causes a re-read and re-evaluation, not a replayed set of actions.
- Cancellation and return paths apply the intended actions and leave payment state alone.
- Reconciliation is safe to re-run and stops cleanly at its checkpoint.
postDeploycreates one Subscription, a repeated deployment does not duplicate it, andpreUndeployremoves it.
After deployment, trace a real Order end to end:
- Confirm that one Subscription exists with the expected key, destination, and Message types.
- Place an Order and confirm that it appears in the OMS with the expected mapping.
- Confirm that the Order in commercetools carries the OMS identifier in its SyncInfo.
- Drive a status change, a shipment with tracking, and a delivery in the OMS, and confirm each one on the Order.
- Exercise a cancellation and a return, and confirm the resulting state and Inventory effect.
- Redeliver the same export Message and repeat the same inbound update, and confirm that neither produces a duplicate.
- Make the OMS fail temporarily, and confirm that the work is retried rather than lost.
- Run reconciliation against a deliberately missed update and confirm that it repairs the drift.
sandbox deployment scales to zero and takes approximately 15 seconds to start again, which exceeds the 10-second acknowledgment deadline, so early events are redelivered until the application is warm. Subscription delivery has no guaranteed time frame and can be delayed by several minutes. Order-driven and reservation-driven Inventory changes can lag by up to 10 seconds. Confirm the deployment environment and the elapsed time before treating any of these as a defect.resource.id with sequenceNumber for Messages, and your own key for inbound requests. Log identifiers and decisions rather than payload bodies, so Customer data does not reach the logs. Define how an operator replays a failed Order after a fix, and record it alongside the Connector.Troubleshoot observable symptoms
Match the symptom you observe to its likely cause and resolution:
| Symptom | Likely causes | Checks and resolution |
|---|---|---|
| No events reach the export | The Subscription is missing. The scripts block was never declared in connect.yaml. Or postDeploy built a destination for the wrong broker and Subscription creation failed. Or delivery stopped after a configuration error. | Query Subscriptions by key and inspect the health status. Compare the destination with CONNECT_SUBSCRIPTION_DESTINATION. Review deployment logs. |
| Most Orders export, but subscription or migrated Orders never do | The Subscription covers Order Created only. | Add Order Created From Recurring Order and Order Imported to the Subscription and the handler. |
| The same Order retries for hours and then disappears | The handler returns a status outside the acknowledgment set for a Message that can never succeed. | Acknowledge permanently unprocessable Messages, record them for replay, and alert instead of looping. |
| Logs show success, but the OMS has no record | The handler acknowledged before the OMS call, or an error was swallowed and a success status returned. | Check OMS-side activity, not only Connector logs. Distinguish transient failures from terminal ones and let transient failures redeliver. |
| The largest Orders fail while smaller ones succeed | The Message exceeded the queue size limit and arrived with payloadNotIncluded. | Retrieve the Order by resource.id instead of reading the payload. |
| Export volume climbs with no matching Order volume | The export subscribes to lifecycle Messages that the inbound flow produces. | Filter changes made by your own API Client, or exit early when the Order already matches the reported state. |
| The OMS created the Order, but the correlation write failed | The Channel used in Update SyncInfo lacks the OrderExport or OrderImport role. | Add the role to the Channel, or create a dedicated export Channel, then replay the affected Orders. |
| Inbound updates return a version conflict on every attempt | Concurrent writes to the Order, or a retry that replays the same actions against a refreshed version. | Re-read the Order and re-evaluate the decision on each attempt instead of resending the original actions. |
| One shipment appears as two Deliveries | The inbound handler is not idempotent, or a conflict retry re-applied Add Delivery. | Check current Deliveries before adding one, and key the check on the OMS shipment identifier. |
| Delivered quantities exceed the quantities ordered | Duplicate or miscounted Deliveries. The API does not validate cumulative quantities. | Validate the running total in the handler before adding a Delivery. |
| An Order regresses to an earlier status | A late notification overwrote a newer state. | Guard the transition on the OMS sequence or the current Order state. |
| A cancellation update is rejected | Cancelled was sent to shipmentState, or Canceled to orderState. | Use Cancelled for OrderState and Canceled for ShipmentState. |
| A shipment update fails on a valid Order | The Order has no shippingInfo or shipping, which Add Delivery requires. | Confirm the shipping data exists before applying the Delivery, and fix the checkout flow that omitted it. |
| Stock stays depleted after a cancellation | No system restocked the canceled quantity. | Confirm the restock behavior in your Project, and have the OMS send an explicit Inventory update when it owns the restock. |
| Reconciliation reports drift that disappears on the next run | The comparison ran inside the 10-second Inventory consistency window. | Trail the comparison window behind the current time. |
| Reconciliation double-processes records | Two scheduled runs overlapped. | Take a durable lock with a time to live longer than the 30-minute job timeout. |
| Events are redelivered only in testing | A sandbox deployment scaled to zero and took approximately 15 seconds to start. | Test against a warm deployment, or account for the cold start in the acknowledgment budget. |
| Deployment succeeds but requests fail at runtime | The API Client scopes do not cover the enabled flows. | Compare the flows against the scope table and keep the final set least-privileged. |
| The Connector cannot be deployed at all | The chosen integration is vendor-hosted rather than an installable Connector. | Follow the vendor's setup instructions; there is nothing to deploy in Connect. |