# Integrate an order management system 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. This guide does not cover master data. Products, Categories, and Prices belong to [Integrate product data](/guides/product-data-integrations.md), which also covers the initial Inventory load. B2B accounts, contract Prices, and financial documents belong to [Integrate ERP](/guides/erp-integration.md). When one system is both your ERP and your OMS, apply those guides to master data and this guide to the post-order lifecycle. This guide also does not cover payment capture or refund, which belong to your payment integration. Copying Orders to a data warehouse or analytics tool for reporting is a separate one-way export covered by [Integrate an analytics destination](/guides/analytics-export-integration.md). Carrier rate quoting during checkout belongs to [Integrate a shipping carrier or rate service](/guides/shipping-integration.md). That guide also covers booking labels and recording tracking numbers directly against a carrier, which applies only when no OMS is in the picture. When an OMS books shipments, this guide owns the Delivery, Parcel, and tracking write-back described in [Read the Order before writing to it](/guides/oms-integration.md#read-the-order-before-writing-to-it). Only one system may write that data, because two writers produce duplicate Deliveries and conflicting tracking numbers. Before you start, you need a commercetools Project with Orders being created, an OMS with a documented API, and an agreed owner for each data domain described in the next section. For the wider planning framework, see [Integration planning and patterns](/learning-integrate-with-commercetools/integration-patterns/integration-planning-and-patterns.md). ## 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 Order data in commercetools is captured at checkout. In most implementations the system of record for the Order then lives downstream in the OMS or ERP, and commercetools becomes a read-only reflection of the fulfillment lifecycle. For organizations without an OMS or ERP, commercetools can remain the source of truth for Orders. For the wider decision, see [Plan integrations](/guides/implementation-guide/plan-integrations.md). 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 | Inventory follows the system of record for Orders because Orders consume stock. If the OMS owns Orders, it usually owns Inventory as well. The direction for the Customer profile depends on your Customer integration, so confirm it against [Integrate a CRM](/guides/crm-integration.md) before you rely on the row above. ### 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 `service` application. Polling and batch files map to a `job` application. - **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](/guides/marketplace-integration.md). - 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](/api/projects/states.md). - 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 Check the current Connector inventory instead of relying on a remembered list, because published Connectors and their versions change. Use an API Client with the `view_connectors` scope to call the [Search Connectors](/connect/connectors.md#search-connectors) endpoint and filter by integration type: ```http title="Search for OMS Connectors" GET https://connect.{region}.commercetools.com/connectors/search?integrationTypes=oms&integrationTypes=shipping ``` Query both `oms` and `shipping` to find relevant candidates. The [IntegrationType](/search.md?urn=ctp:connect:type:IntegrationType) enum has no separate fulfillment value. For the host to use in each Region, see [Hosts and authorization](/connect/hosts-and-authorization.md). The same search is available in the Merchant Center. Compare certification and access, source availability, installation requirements, and Region support using the canonical [Connector representation](/connect/connectors.md#connector). Record the Connector key and version you evaluated. A listing in the [technology partner directory](https://marketplace.commercetools.com/connectors) is not automatically an installable Connector. For installable Public Connectors, use [Connect in the Merchant Center](/merchant-center/connect.md). Confirm the deployment model of each candidate before you plan a Connect deployment. The vendor documentation and repository are authoritative for how an integration installs and what it covers. 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. If no Connector matches your OMS, or the OMS is bespoke, build a new one. Decide where the integration runs before you scaffold it; for a comparison of vendor middleware, integration platforms, cloud services, and Connect, see [Compare middleware and runtime options](/guides/erp-integration.md#compare-middleware-and-runtime-options). The [Connect CLI](/connect/cli.md) ships a `fulfilment-integration` template that scaffolds the application structure for these flows: ```bash title="Initialize a fulfillment Connector" commercetools connect init my-oms-connector --template fulfilment-integration ``` The template is not listed on the [Application templates overview](/connect/templates/templates-overview.md), which documents the payment, product export, tax, and email templates. It declares four applications: an `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. The `fulfilment-integration` template supplies application structure, not a working OMS integration. This assessment covers [commit 30ce085](https://github.com/commercetools/connect-fulfilment-integration-template/tree/30ce0858756a2116b45097684f8c4b4b20d58b53), 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 `scripts` block in `connect.yaml`. The `postDeploy` and `preUndeploy` files exist in each application, but they are not declared, so a deployment creates no Subscription. - Authentication on the inbound `order-updates` and `inventory-import` endpoints. 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](/search.md?urn=ctp:api:type:OrderAddDeliveryAction) and [Change ShipmentState](/search.md?urn=ctp:api:type:OrderChangeShipmentStateAction). - 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 An OMS integration is a directional data sync between two systems that each own part of the Order lifecycle. This guide uses the standard [Connect](/connect/overview.md) application types: an `event` application for the export, a `service` application for inbound updates, and an optional `job` application for reconciliation. ### Export placed Orders The export reacts to an Order and pushes it to the OMS. Use an `event` application, which receives Messages through a Subscription and a Connect-provisioned message broker. ```mermaid title="Export a placed Order to the OMS" sequenceDiagram participant CT as commercetools participant Sub as Subscription participant Export as Connect event application participant OMS as Order management system CT->>Sub: Emit Order Message Sub->>Export: Deliver encoded envelope Export->>Export: Decode and filter by Message type Export->>CT: Get Order by resource.id CT-->>Export: Current Order and version Export->>OMS: Look up by orderNumber OMS-->>Export: Existing record or none Export->>OMS: Create the Order if absent OMS-->>Export: OMS identifier Export->>CT: Update SyncInfo with the returned version Export-->>Sub: Acknowledge within 10 seconds ``` #### Subscribe to every Message that represents an exported Order Direct checkout, Recurring Order, and import flows do not necessarily emit the same Message. Select the required types from [Cart and Order Messages](/api/projects/messages/cart-order-messages.md). An incomplete selection can leave some Orders unexported without producing an error. Add Order lifecycle Messages only when the OMS must also receive changes made in commercetools. Subscribe to the smallest set that meets your requirements, and acknowledge anything else without acting on it. #### Retrieve the Order instead of reading the Message payload Retrieve the Order by `resource.id`. Subscriptions deliver [at least once with no ordering guarantee](/api/projects/subscriptions.md#delivery-guarantees), 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 The same Message can arrive more than once. Send the commercetools `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](/search.md?urn=ctp:api:type: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 An Order has no top-level `externalId` field. Use [SyncInfo](/search.md?urn=ctp:api:type:SyncInfo), which exists to record synchronization activity such as an export, and write it with the [Update SyncInfo](/search.md?urn=ctp:api:type:OrderUpdateSyncInfoAction) update action. The action requires a [Channel](/search.md?urn=ctp:api:type: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](/api/predicates/query.md). A [Custom Field](/api/projects/custom-fields.md) is a valid alternative when you need to store more than an identifier and a Channel. #### Acknowledge deliberately Event applications and API Extensions use different response contracts. Follow [Event application behavior](/connect/deployment-behavior-and-environments.md#event) and [API Extension responses](/api/projects/api-extensions.md#response), and do not reuse acknowledgment logic between the two application types. The message queue treats `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 | Returning `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 The inbound flow applies update actions that produce their own Messages: [Change OrderState](/search.md?urn=ctp:api:type:OrderChangeOrderStateAction) produces Order State Changed, and [Change ShipmentState](/search.md?urn=ctp:api:type:OrderChangeShipmentStateAction) produces Order Shipment State Changed. If your export Subscription covers those types so that the OMS receives changes made in commercetools, the inbound flow re-triggers the export and export volume climbs continuously with no business cause. Filter changes made by your own API Client, or exit early when the Order already matches the state the OMS reported. ### Apply inbound updates Inbound updates arrive from the OMS. Use a `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. ```mermaid title="Apply an OMS update to the Order" sequenceDiagram participant OMS as Order management system participant Inbound as Connect service application participant CT as commercetools OMS->>Inbound: Send update with the OMS identifier Inbound->>Inbound: Authenticate caller and validate payload Inbound->>CT: Query Orders by syncInfo predicate CT-->>Inbound: Order, current state, and version Inbound->>Inbound: Compare OMS sequence against current state Inbound->>CT: Post update actions with that version CT-->>Inbound: Updated Order, or a version conflict Inbound-->>OMS: Return a status the OMS can act on ``` #### 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 Find the Order with the `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 Every update action requires the current version, and some require existing state. [Add Delivery](/search.md?urn=ctp:api:type:OrderAddDeliveryAction) fails unless the Order already has `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 A redelivered "shipped" notification must be a no-op, and a notification that arrives late must not overwrite a newer state. Compare the sequence or version the OMS supplies against the current state of the Order, and skip the write when the state is already correct or has moved past it. Handle a `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. ```typescript title="Apply an OMS shipment update idempotently" 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}`); } ``` In `Multiple` [ShippingMode](/search.md?urn=ctp:api:type:ShippingMode), read the Deliveries from `shipping[].shippingInfo.deliveries` and pass the matching `shippingKey` in the [Add Delivery](/search.md?urn=ctp:api:type:OrderAddDeliveryAction) 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 A scheduled `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. ```mermaid title="Reconcile Orders and Inventory on a schedule" flowchart LR Start[Scheduled run] --> Lock{Acquire lock} Lock -- Held by an active run --> Skip[Log and exit] Lock -- Acquired --> Cursor[Load checkpoint] Cursor --> Query[Query Orders with missing or stale SyncInfo] Query --> Handle[Run the same idempotent export handler] Handle --> Inventory[Compare Inventory snapshot with the OMS] Inventory --> Save[Checkpoint the page] Save --> More{More pages and time remaining} More -- Yes --> Query More -- No --> Release[Release lock] ``` Follow the documented [Job application behavior](/connect/deployment-behavior-and-environments.md#job) and [resource recommendations](/connect/best-practices.md#job-application). The Connect documentation does not guarantee that scheduled runs cannot overlap, so take a durable lock with a time to live longer than the maximum run time. A crashed run then eventually releases the lock. Checkpoint your cursor after each page so the next run resumes instead of restarting, and stop with a margin before the timeout. A job should orchestrate large batch work rather than perform it in the container. 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 The mapping between the two status vocabularies is the most error-prone part of the integration, because commercetools separates overall Order, shipment, payment, custom workflow, and per-Line Item state. See the [Order representations](/api/projects/orders.md#representations) and [Order update actions](/api/projects/orders.md#update-actions) for the canonical fields and actions. The built-in Order and shipment vocabularies differ, including the spelling of cancellation: [OrderState](/search.md?urn=ctp:api:type:OrderState) uses `Cancelled`, while [ShipmentState](/search.md?urn=ctp:api:type: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](/search.md?urn=ctp:api:type:OrderTransitionStateAction) | | Allocated or picking | Line Item State | [Transition LineItem State](/search.md?urn=ctp:api:type:OrderTransitionLineItemStateAction) | | Shipped, with tracking | `shipmentState` `Shipped`, plus a Delivery and Parcel | [Change ShipmentState](/search.md?urn=ctp:api:type:OrderChangeShipmentStateAction), [Add Delivery](/search.md?urn=ctp:api:type:OrderAddDeliveryAction), [Add Parcel to Delivery](/search.md?urn=ctp:api:type:OrderAddParcelToDeliveryAction), [Set Parcel Tracking Data](/search.md?urn=ctp:api:type:OrderSetParcelTrackingDataAction) | | Partially shipped | `shipmentState` `Partial`, plus one Delivery per shipment | [Change ShipmentState](/search.md?urn=ctp:api:type:OrderChangeShipmentStateAction), [Add Delivery](/search.md?urn=ctp:api:type:OrderAddDeliveryAction) | | Delivered | `orderState` `Complete` | [Change OrderState](/search.md?urn=ctp:api:type:OrderChangeOrderStateAction) | | Cancelled | `orderState` `Cancelled` | [Change OrderState](/search.md?urn=ctp:api:type:OrderChangeOrderStateAction) | | Return initiated or received | Return Item with `Advised` or `Returned` state | [Add ReturnInfo](/search.md?urn=ctp:api:type:OrderAddReturnInfoAction) | | Returned item assessed | Return shipment state `BackInStock` or `Unusable` | [Set ReturnShipmentState](/search.md?urn=ctp:api:type:OrderSetReturnShipmentStateAction) | When your required statuses do not exist as built-in `orderState` values, model them as a custom [State](/search.md?urn=ctp:api:type:State) machine and register the States and their transitions idempotently during deployment. Split and partial fulfillment need particular care. The API does not validate cumulative Line Item quantities across Deliveries, as described in [Multiple deliveries](/api/shipping-delivery-overview.md#multiple-deliveries). A duplicate or miscounted inbound update therefore produces an Order that reports more items delivered than were bought, without any error. Your handler owns that check. Returns are also two decisions, not one. [ReturnShipmentState](/search.md?urn=ctp:api:type:ReturnShipmentState) tracks the physical return, and [ReturnPaymentState](/search.md?urn=ctp:api:type:ReturnPaymentState) tracks whether the money was refunded. Set the payment state from an authoritative payment event rather than inferring a refund from the arrival of the goods. ## 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. Decide first whether commercetools also writes Inventory. The [InventoryMode](/search.md?urn=ctp:api:type:InventoryMode) of a Cart or Line Item controls this: `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](/api/inventory-overview.md#inventory-modes) for the full behavior of each mode. For OMS upserts, define a consistent SKU and supply location key convention. See [Manage Inventory with InventoryEntry](/api/inventory-overview.md#manage-inventory-with-inventoryentry) and [Supply Channels versus Distribution Channels](/api/inventory-overview.md#supply-channels-versus-distribution-channels) for the canonical Inventory model. The initial bulk Inventory load and its mapping onto the catalog belong to [Integrate product data](/guides/product-data-integrations.md); this section covers only the steady-state updates the OMS owns. When the OMS owns Inventory, apply its updates with [Change Quantity](/search.md?urn=ctp:api:type:InventoryEntryChangeQuantityAction) for an absolute level, or [Add Quantity](/search.md?urn=ctp:api:type:InventoryEntryAddQuantityAction) and [Remove Quantity](/search.md?urn=ctp:api:type:InventoryEntryRemoveQuantityAction) for a delta. Absolute levels are safer under at-least-once delivery, because a repeated absolute update is a no-op while a repeated delta is not. Inventory thresholds can emit Messages that trigger an asynchronous availability check against the system of record. They do not perform a blocking OMS check. For the threshold pattern that reduces per-Order OMS availability calls, see [Plan integrations](/guides/implementation-guide/plan-integrations.md). 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](/api/inventory-overview.md#inventory-checks-and-consistency). - **Decide which system restocks canceled Inventory.** Changing `orderState` to `Cancelled` records 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 Declare them through `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](/connect/modify-connector.md) and the canonical [API scopes](/api/scopes.md). ### Separate configuration from secrets Classify OMS settings according to the canonical [`connect.yaml` configuration rules](/connect/development.md#configure-connectyaml). Never place OMS credentials or webhook secrets in code or logs. ### Build the Subscription destination from the injected variables Connect deploys to both Google Cloud and AWS Regions, and the message broker follows the Region. Build the destination from the Event variables documented in [Automation scripts](/connect/automation-scripts.md), and support both brokers. Verify Subscription creation separately because Deployment status alone does not establish that automation-created resources exist. ### Register resources idempotently Create the Subscription, any custom States, and any Custom Types in `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 Query Subscriptions for the expected key after every deployment, and check [Deployment logs](/connect/deployment-logs.md). This verification confirms whether the automation script created the expected resource. ### Validate the OMS connection at deploy time Have `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. - `postDeploy` creates one Subscription, a repeated deployment does not duplicate it, and `preUndeploy` removes it. After deployment, trace a real Order end to end: 1. Confirm that one Subscription exists with the expected key, destination, and Message types. 2. Place an Order and confirm that it appears in the OMS with the expected mapping. 3. Confirm that the Order in commercetools carries the OMS identifier in its SyncInfo. 4. Drive a status change, a shipment with tracking, and a delivery in the OMS, and confirm each one on the Order. 5. Exercise a cancellation and a return, and confirm the resulting state and Inventory effect. 6. Redeliver the same export Message and repeat the same inbound update, and confirm that neither produces a duplicate. 7. Make the OMS fail temporarily, and confirm that the work is retried rather than lost. 8. Run reconciliation against a deliberately missed update and confirm that it repairs the drift. Some expected behavior looks like failure during testing. A default `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. In production, monitor Subscription health, acknowledgment failures, processing duration for each flow, version conflict rates, reconciliation lag, and the count of Orders with missing SyncInfo. Log a correlation key on every line: use `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. | ## Related pages - [Area overview page with navigation](/guides.md) - [Previous page: Integrate ERP](/guides/erp-integration.md) - [Next page: Integrate shipping](/guides/shipping-integration.md) - [Search documentation and API specs](/search.md)