Announcements
Configuration & Discovery
Checkout Session Authentication
Technical Audit
Implementation Considerations
Transaction Management
Enabling Google as a Platform
Connect a Third-Party OMS
B2C Commerce Release Notes
Ask the Community
Audience: Merchants and system integrators connecting an external Order Management System to Salesforce B2C Commerce.
Related: This is a Backend-only Commerce App. For the framework it builds on, see Architecture Overview and Building Your Commerce App.
B2C Commerce is a high-scale order-taking engine: it captures orders at peak traffic and is the point of origin for every order placed on your digital storefronts and channels. Once an order is created, the work of post-purchase processing — fulfillment, shipping, cancellations, returns, refunds — is typically handled by an Order Management System (OMS).
Salesforce offers a tightly integrated OMS (Salesforce Order Management) for exactly this. But you are not limited to it: you can now plug in a third-party OMS and have it serve order data through the same integrated surfaces B2C Commerce already exposes — the Shopper Orders and Shopper Customers SCAPIs, as well as the Universal Commerce Protocol (UCP) order lifecycle updates. The shopper- and agent-facing experience is identical no matter which OMS is behind it.
This guide shows you how to make that connection. The standard GET endpoints return the order as
it was created in B2C Commerce, but carry no real-time data from the integrated OMS. That gap is
closed with the expand=oms parameter on certain shopper-facing APIs, which loads the current
order data from the OMS. When Salesforce Order Management is integrated, this works automatically.
When you want to integrate a third-party OMS instead, the rest of this document describes how.
You do this by building a Commerce App whose backend (site) cartridge implements a small set of server-side hooks. When your app registers the hooks and the integration is enabled, the platform routes order operations to your hooks automatically. Your hooks are responsible for one thing: talking to your OMS and mapping its data to the platform’s order shape. Everything else — shopper authentication, the access decision, request/response validation, the public API surface — stays with the platform.
Nothing changes for API consumers. Storefronts, the mobile app, and agentic-commerce channels keep calling the same SCAPI Shopper Orders endpoints. The response shape is identical whether the data came from the built-in store or your OMS.

When a request arrives, the platform authenticates the shopper and loads the order from its own store if it still holds it. It then hands your hook an order document — pre-populated with the platform’s data when the order was found locally, or empty when it was not — for your hook to augment. Your hook decides what it needs from the OMS: it can fill in a document the platform left empty, or enrich a pre-populated one with fields the platform doesn’t have (live fulfillment status, tracking, and so on), and it can skip the OMS call entirely when the platform’s data already suffices. The platform then takes over again: it applies access control, and, for a cancel or return, calls your cancel or return hook, which executes the operation against the OMS and returns the updated order. Finally the platform serializes the resulting document as the SCAPI response.
Your hooks own every interaction with the OMS — enriching order data on reads and executing the actions on writes; everything around them — authentication, access control, validation, and the response — is the platform’s. Access control, in particular, is only exercised for an order the platform did not already load and access-check itself: an order the platform served from its own store is already trusted, whereas an order that exists only in the OMS (or one your list hook adds) is access-checked before it is returned. That check uses a sensible default (the shopper must own the order), but when it doesn’t fit your needs a custom access-control hook can fully override it — at which point the platform steps aside and you own and enforce the access decision yourself.
The integration is made up of two groups of hooks: four core hooks that form the integration itself, and two optional hooks that let you refine specific behaviors.
These four are the integration. The platform routes order operations to your OMS only when all four are registered together and the feature toggle is on. If any one is missing, the platform ignores the others and falls back to its default behavior. Register all four even if some start out as temporary no-ops during development.
sfcc.app.oms.order.retrieve — provides a single order. The platform hands you the order
document — pre-populated when it still holds the order locally, empty when it does not — and your
hook augments it from the OMS: enriching it with data the platform lacks (live fulfillment status,
tracking, …) or filling it entirely when the platform had nothing. This is the workhorse of the
integration: it runs when a shopper views an order, up front on every cancel, return, and
access-code request, and whenever the platform needs the current order internally — for example to
assemble an order snapshot for UCP webhook events.sfcc.app.oms.order.list — provides orders belonging to a single customer for their order
history. The platform pre-populates the result with the orders it already holds for that customer,
and your hook augments the list — typically appending the customer’s OMS-only orders. This is the
only hook that is customer-scoped by nature, and it may need to return a specific subset of orders
(paging).sfcc.app.oms.order.cancel — cancels a whole order in your OMS. The platform calls it after
it has loaded the order and confirmed the shopper is allowed to cancel it.sfcc.app.oms.order.return — starts a return for specific order items, called under the same
post-access-check conditions as cancel.Both of these are independent — register only the ones you need; they do not affect whether the integration is active.
sfcc.app.oms.order.accessControl — overrides the platform’s default ownership decision.
Register it only when “does this shopper own this order?” can’t be answered by matching the
platform customer id (for example, when your OMS uses a different customer identifier and you
manage it via custom attributes on the customer profile). It runs after
a retrieve and fully replaces the default check.sfcc.app.oms.order.reasonCodes — supplies the cancel and return reason codes your OMS
accepts. The platform uses them both to serve the oms-meta-data endpoint (so the storefront can
render a reason picker) and to validate a submitted reason.Reference: The
sfcc.app.oms.order.*extension points are documented with full contract details, parameter types, and return values in the B2C Commerce Script API documentation.
dw.svc.LocalServiceRegistry).The integration is active for a site only when both of these are true:
OmsAppHooksEnabled).sfcc.app.oms.order.retrieve, sfcc.app.oms.order.list,
sfcc.app.oms.order.cancel, and sfcc.app.oms.order.return.This is all-or-nothing: if any of the four is missing, the platform does not route order operations to your app and falls back to its default behavior. Register all four even if some are temporary no-ops during development.
Triggering the hook on reads. For
getOrderandgetCustomerOrders, the storefront request must include the query parameterexpand=omsto route to your retrieve hooks. Without it, the platform serves the request from its own order store as usual.
Declare the hooks in your cartridge’s hooks.json, and point package.json at it.
1// hooks.json
2{
3 "hooks": [
4 { "name": "sfcc.app.oms.order.retrieve", "script": "./oms/retrieve.js" },
5 { "name": "sfcc.app.oms.order.list", "script": "./oms/listOrders.js" },
6 { "name": "sfcc.app.oms.order.cancel", "script": "./oms/cancel.js" },
7 { "name": "sfcc.app.oms.order.return", "script": "./oms/return.js" }
8 ]
9}1// package.json
2{ "hooks": "./hooks.json" }Add optional hooks as extra entries:
1{ "name": "sfcc.app.oms.order.accessControl", "script": "./oms/accessControl.js" },
2{ "name": "sfcc.app.oms.order.reasonCodes", "script": "./oms/reasonCodes.js" }Each hook takes two arguments — an input and an order document to work with — and returns a
dw.system.Status. For most hooks the input is a purpose-built context object (for example
OmsOrderContext, carrying siteId and orderNo). The one exception is accessControl, whose
first argument is the authenticated shopper as a standard dw.customer.Customer object rather than a
context.
The order document is not always empty. On the read hooks the platform pre-populates it with the order it already holds locally, and hands you an empty document only when it has none. Your hook can therefore either enrich the data already there or fill a blank document — inspect it and add only what the OMS needs to contribute.
new Status(Status.OK) — success.new Status(Status.ERROR, ...) — failure. On a single-order retrieve this surfaces as 404.Always return a
Status. A hook that returns nothing is treated as a failure, not a success.
The order documents you populate are Script API work objects. Use snake_case property
names in your hook (order_no, customer_info, product_items, tracking_number). The platform
serializes them to the camelCase SCAPI response (orderNo, customerInfo, productItems,
trackingNumber) automatically.
Signature: retrieve(context, orderResponse).
The context identifies the order by the combined key context.siteId + context.orderNo. It does
not carry a customer id — a retrieve hook is a pure data provider and must not implement its own
ownership logic; the platform handles access control.
orderResponse arrives pre-populated when the platform still holds the order locally, and empty
when it does not. So there are two things your hook typically does:
Detect which case you’re in by checking whether the document already carries an order number.
1"use strict";
2var Status = require("dw/system/Status");
3
4function retrieve(context, orderResponse) {
5 var alreadyLoaded = !!orderResponse.orderNo; // platform pre-populated it
6
7 var omsOrder = MyOms.getByOrderNo(context.siteId, context.orderNo);
8
9 // Order isn't in the platform AND isn't in the OMS → not found.
10 if (!alreadyLoaded && !omsOrder) {
11 return new Status(Status.ERROR, "ORDER_NOT_FOUND", "Order not found");
12 }
13
14 if (!alreadyLoaded) {
15 // Empty document: fill the base order from the OMS.
16 orderResponse.orderNo = omsOrder.number;
17 orderResponse.status = "open"; // must map to an allowed status enum value
18 orderResponse.orderTotal = omsOrder.total;
19 orderResponse.currency = omsOrder.currency;
20 orderResponse.guest = omsOrder.isGuest;
21
22 // customer_id must be the platform customer id — the default access check compares against it.
23 orderResponse.customerInfo = {
24 customer_id: omsOrder.platformCustomerId,
25 customer_no: omsOrder.customerNo,
26 email: omsOrder.email,
27 };
28 orderResponse.productItems = omsOrder.lines.map(function (l) {
29 return {
30 product_id: l.sku,
31 product_name: l.name,
32 quantity: l.qty,
33 price: l.price,
34 item_id: l.lineId, // stable line id — needed to return specific items
35 shipment_id: l.shipmentId, // groups line items; NOT fulfillment/tracking data
36 };
37 });
38 // A full order also carries payment instruments, promotions, gift certificates, taxes, etc.
39 // Map whatever your storefront's order experiences need — omitted here for brevity.
40 }
41
42 // In both cases, enrich with the live post-purchase data the platform doesn't hold. Put
43 // fulfillment/tracking under omsData.shipments — the shipment object that carries tracking
44 // links, carrier, and delivery dates. (order.shipments is line-item grouping, not this.)
45 if (omsOrder) {
46 orderResponse.omsData = {
47 shipments: omsOrder.shipments.map(function (s) {
48 return {
49 id: s.id,
50 status: s.status, // e.g. 'shipped', 'delivered'
51 tracking_number: s.tracking,
52 tracking_url: s.trackingUrl,
53 provider: s.carrier,
54 };
55 }),
56 };
57 }
58
59 return new Status(Status.OK);
60}
61
62exports.retrieve = retrieve;Signature: list(context, customerOrdersResponse).
This is the one hook that is customer-scoped by nature. The context carries the authenticated shopper’s identifiers and paging/filter inputs:
| Field | Notes |
|---|---|
context.customerId | Platform customer id of the authenticated shopper. |
context.start, context.count | Paging window. Your hook owns pagination against your OMS. |
context.from, context.until | Optional date filters. |
context.status | Optional status filter. |
context.siteId | The site. |
context.crossSites | Whether the request spans sites. |
customerOrdersResponse arrives pre-populated with the orders the platform already holds for
this customer, in its data list. Your hook augments that list — typically appending the
customer’s OMS-only orders rather than replacing what’s there. Add only orders belonging to
context.customerId. The platform trusts the entries it put there and access-checks the ones you
add.
1"use strict";
2var Status = require("dw/system/Status");
3
4function listOrders(context, customerOrdersResponse) {
5 var page = MyOms.getOrdersForCustomer(
6 context.customerId,
7 context.start,
8 context.count,
9 context.siteId,
10 );
11
12 // Append the OMS orders to whatever the platform already loaded.
13 var existing = customerOrdersResponse.data || [];
14 var omsOrders = page.orders.map(function (o) {
15 return {
16 order_no: o.number,
17 status: "completed",
18 creation_date: o.createdAt, // a java.util.Date
19 order_total: o.total,
20 currency: o.currency,
21 customer_info: { customer_id: o.platformCustomerId, customer_no: o.customerNo },
22 product_items: o.lines.map(function (l) {
23 return {
24 product_id: l.sku,
25 product_name: l.name,
26 quantity: l.qty,
27 price: l.price,
28 item_id: l.lineId,
29 };
30 }),
31 };
32 });
33
34 customerOrdersResponse.data = existing.concat(omsOrders);
35 customerOrdersResponse.count = customerOrdersResponse.data.length;
36
37 return new Status(Status.OK);
38}
39
40exports.list = listOrders;Signature: cancel(context, orderResponse). Context carries orderNo, siteId, and reason
(already validated and defaulted by the platform).
Before the platform calls this hook it has already loaded the order (via your retrieve hook) and
run it through access control, and it hands that populated order to you as orderResponse. You can
inspect it to perform your own eligibility checks (for example, refuse to cancel an order that has
already shipped). If the order is eligible, forward the cancellation to your OMS, then update the
same orderResponse in place to reflect the post-cancellation state — the platform returns it as
the response.
1"use strict";
2var Status = require("dw/system/Status");
3
4function cancel(context, orderResponse) {
5 // orderResponse is the already-loaded, access-checked order — use it for eligibility checks.
6 var shipments = orderResponse.omsData ? orderResponse.omsData.shipments : null;
7 if (shipments && shipments.length > 0) {
8 return new Status(Status.ERROR, "NOT_CANCELABLE", "Order has already shipped");
9 }
10
11 var result = MyOms.cancelOrder(context.orderNo, context.reason);
12 if (!result.ok) {
13 return new Status(Status.ERROR, "OMS_CANCEL_REJECTED", result.message);
14 }
15
16 // Update the loaded order to its post-cancellation state.
17 orderResponse.status = "cancelled";
18 return new Status(Status.OK);
19}
20
21exports.cancel = cancel;Signature: returnOrder(context, orderResponse). The context carries productItems — the items to
return, each with itemId, quantity, and a validated reason. As with cancel, orderResponse is
the already-loaded, access-checked order: inspect it for eligibility, forward the return to your OMS,
then update it in place to reflect the post-return state.
Because return is a reserved word in JavaScript, name the function anything else and export it
under the return key:
1"use strict";
2var Status = require("dw/system/Status");
3
4function startReturn(context, orderResponse) {
5 var result = MyOms.createReturn(context.productItems);
6 if (!result.ok) {
7 return new Status(Status.ERROR, "OMS_RETURN_REJECTED", result.message);
8 }
9 orderResponse.status = "completed";
10 return new Status(Status.OK);
11}
12
13exports["return"] = startReturn; // NOT exports.returnSignature: accessControl(customer, orderResponse), where customer is a dw.customer.Customer.
By default the platform grants access when the order’s customer_info.customer_id matches the
authenticated shopper. Register this hook only when your ownership rule is different — for example
when your OMS uses a different customer identifier and you manage it via custom attributes on the
customer profile. When registered, this hook fully replaces the default check. Return
Status.OK to grant, Status.ERROR to deny.
1"use strict";
2var Status = require("dw/system/Status");
3
4exports.accessControl = function (customer, orderResponse) {
5 // The OMS uses its own customer identifier, stored as a custom attribute on the profile.
6 var externalCustomerId = customer.custom.externalOmsCustomerId;
7 if (externalCustomerId && externalCustomerId === orderResponse.customerInfo.customer_id) {
8 return new Status(Status.OK);
9 }
10 return new Status(Status.ERROR);
11};Signature: reasonCodes(context, omsMetaData). The context carries context.siteId (use it if
your OMS has per-site reason codes). Fill cancelReasonCodes and returnReasonCodes, each an array
of { reason, default }. Mark exactly one entry per list as default: true.
1"use strict";
2var Status = require("dw/system/Status");
3
4function reasonCodes(context, omsMetaData) {
5 // context.siteId identifies the site the codes are requested for.
6 var codes = [
7 { reason: "Not specified", default: true },
8 { reason: "Defect", default: false },
9 { reason: "Wrong item", default: false },
10 ];
11 omsMetaData.cancelReasonCodes = codes;
12 omsMetaData.returnReasonCodes = codes;
13 return new Status(Status.OK);
14}
15
16exports.reasonCodes = reasonCodes;The platform serves these codes on the oms-meta-data endpoint and validates cancel/return reasons
against them.
Your hooks map your OMS’s data onto the platform’s order shape. The full set of available fields is defined by the SCAPI Order schema in the Shopper Orders API reference; you decide which of them your storefront’s order experiences need and populate those — unset fields are simply absent from the response.
Two things always hold, regardless of which fields you populate:
snake_case in the hook, camelCase on the wire — for example set
order_no, customer_info, product_items; the platform serializes them as orderNo,
customerInfo, productItems.customer_info.customer_id is load-bearing for the default access check. It must carry the
platform customer id of the shopper who owns the order, because the default access check
compares it against the authenticated shopper. This has an
upstream consequence: when an order is exported to your OMS, the OMS must persist the platform
customer id alongside the order, so that your retrieve and list hooks can map it back into
customer_info.customer_id on load. Storing it at creation time is what makes the default access
check work; if your OMS instead keys the shopper on a different identifier, map that identifier and
register a custom accessControl hook.Some fields in the Order schema are enums — the OAS spec restricts them to a fixed set of allowed
values. status is the most common example. Where a field is an enum, you must map your OMS’s native
value onto one of the allowed values; anything else is rejected. Check the Order schema in the
Shopper Orders API reference for the exact enum values on each field before you map to it.
The platform — never your retrieve hook — decides whether the authenticated shopper may see an
order. It does so either with its built-in checks or, when you register one, by delegating to your
custom accessControl hook.
Access control runs only for orders the platform did not already load and check itself. When the
platform served the order from its own store, it was already access-checked during that load and is
trusted as-is — the OMS access decision below does not run again for it. The access decision
below applies to orders the platform did not hold: an order that exists only in the OMS (so your
retrieve hook filled an empty document), or an order your list hook appended. For those, after
your hook returns, the platform resolves access in this order:
accessControl hook is not consulted).accessControl
hook, the platform calls it and uses its verdict; the default check does not run. If you did not,
the platform applies the default check: the order’s customer_info.customer_id must match the
authenticated shopper’s platform customer id.If none grants access, the platform returns 404 Not Found — order existence is never leaked to a shopper who does not own it.
Guest orders via OTP. Step 1 is an alternative access path evaluated up front, and it applies only to guest orders — the platform requires the order to be flagged as a guest order (your hook set
guest = truewith a non-nullcustomer_info.customer_id). For such an order, a valid access code (OTP) grants access there and then — neither youraccessControlhook nor the platform’s default ownership check is invoked. Orders owned by a registered customer cannot be accessed via OTP; for them the access code is ignored and access falls to step 2.
Because the platform owns this decision, your single-order hooks (retrieve, cancel, return)
receive no customer id and must not re-check ownership. The list hook does receive
context.customerId — it is customer-scoped by design and needs it to query your OMS for the right
customer’s orders. Even so, the platform does not take the orders you add to the list on trust: it
runs the access decision individually for each order your hook appended — your accessControl
hook if registered, otherwise the default ownership check — and filters out any that does not pass.
(The orders the platform pre-populated are already access-checked and are kept as-is.) So the
customerId on the list context is a query aid, not the access control itself.
For cancel and return, the platform first calls your retrieve hook, verifies ownership, and only
then calls your cancel / return hook — so a mutation is never forwarded to your OMS for an order
the shopper does not own.
When a shopper cancels an order or returns items, the request may carry a reason. If your OMS has
its own set of accepted reasons, register the optional reasonCodes hook to supply them. The
platform uses those codes two ways:
GET .../orders/oms-meta-data endpoint returns them so the storefront can render a reason
picker.The reason-code list is never empty. If you do not register the hook (or it supplies no codes),
the platform injects a single default reason, "Not specified" (default: true). With no hook
registered, "Not specified" is therefore the only accepted reason: an omitted reason resolves to
it, and any other supplied reason is rejected with 400 Bad Request. Register the hook to widen
the accepted set.
Rules:
default-flagged code.Caching. Reason codes are near-static, so the platform does not call this hook on every
request. It caches the hook’s output per site for a short time (a 180-second TTL) and serves both
oms-meta-data and cancel/return validation from that snapshot. When you activate a new code
version, the cache is flushed so an edit to reasonCodes.js takes effect immediately; otherwise a
change is picked up within the TTL window.
Treat
reasonCodesas a settings provider, not a per-request handler. The platform may reuse a cached result instead of calling your hook, so it won’t necessarily run on every shopper request. Just return the list of codes; don’t put logic in it that needs to run each time an order is cancelled or returned. Its only input iscontext.siteId.Even with caching, avoid calling your OMS from inside this hook on every cache miss. Reason codes change rarely, so a live callout per miss adds latency to the first request in each TTL window for no real benefit. Prefer a synchronization approach: periodically pull the codes from your OMS and store them on B2C Commerce (for example in site preferences or a custom object), and have the hook return that locally-stored copy. That keeps even uncached responses fast and insulates the shopper path from OMS availability.
This is a separate, independent SCAPI-only feature. Guest order access codes are a SCAPI-only flow whose purpose is to let a guest shopper regain access to their own orders after the session they placed the order in is gone (guests have no login to prove ownership). It’s not part of the OMS integration — it lives in its own
sfcc.app.order.*namespace and works with any order backend, not just a 3rd-party OMS. This section covers only how the two features interact — see the dedicated guest order access-code documentation for the full feature.
Guests have no account, so once the session they checked out in is gone, they have no way to prove
ownership of their order. To bridge that, the platform issues a one-time, 6-digit access code for
guest order access, valid for a short window. A shopper requests one via
POST .../orders/{orderNo}/actions/request-access-code, and then supplies the code to prove
ownership on the guest read and write paths alike — the order lookup as well as the cancel and return
requests. Note that the field the code goes in differs by endpoint:
POST .../orders/{orderNo}/lookup) — put the code in orderViewCode. That
field accepts either the static OrderViewCode or the 6-digit access code; the server tells them
apart by format. There is no orderAccessCode field on the lookup request..../actions/oms-cancel-order, .../actions/oms-return-order) — put the code
in orderAccessCode.Where it intersects the OMS integration: a valid access code is checked before the OMS
accessControl hook, so it grants access regardless of other rules. For the platform to know which
guest order the code belongs to, your OMS retrieve hook must set guest = true and a non-null
customer_info.customer_id.
When the access-code request is made with expand=oms, the code-generation and email flow runs
against the order data from your OMS (loaded via the retrieve hook) rather than the platform’s
local order. This has a useful side effect: the flow keeps working even after B2C Commerce has
deleted its local copy of the order — for example once the order has been handed off to the OMS
and purged from the platform. As long as the OMS still has the order and your retrieve hook can
return it, a guest can still request an access code and receive the email.
The retrieve hooks are pull-based: the platform calls your OMS when a shopper or channel requests an order. To let your OMS proactively notify the platform when an order ships or is adjusted — so the update propagates to agentic-commerce order webhooks — your OMS calls an inbound Admin API:
1POST /organizations/{organizationId}/orders/{orderNo}/oms-status-events?siteId={siteId}| Property | Value |
|---|---|
| Auth | Account Manager OAuth (client credentials), scope sfcc.orders.rw |
| Success | 202 Accepted (queued; processed asynchronously) |
| Bad request | 400 when the payload is invalid (see the payload rules below) |
| OMS not active | 409 Conflict when the OMS integration is not active for the site — the OMS App Hooks toggle is off, or the required hooks are not all registered |
The request body carries exactly one of a fulfillment event or an adjustment. The platform
assembles the full order (by calling your retrieve hook), merges the pushed change, and publishes
the downstream order webhook.
1{
2 // exactly ONE of the following:
3
4 "fulfillmentEvent": {
5 "id": "ship-001", // required
6 "occurredAt": "2026-06-03T10:15:00.000Z", // required (RFC 3339)
7 "type": "shipped", // required — processing | shipped | in_transit |
8 // delivered | failed_attempt | canceled |
9 // undeliverable | returned_to_sender (open string)
10 "lineItems": [
11 // required, 1..200
12 { "id": "pli-001", "quantity": 2 } // quantity >= 0
13 ],
14 "trackingNumber": "1Z999AA10123456784", // required when type != "processing"
15 "trackingUrl": "https://www.ups.com/track?num=1Z999AA10123456784", // required when type != "processing"
16 "carrier": "UPS", // optional
17 "description": "Handed to carrier" // optional
18 },
19
20 "adjustment": {
21 "id": "ret-7788", // required
22 "type": "return", // required — open string: refund | return | credit | ...
23 "occurredAt": "2026-06-03T10:15:00.000Z", // required (RFC 3339)
24 "status": "completed", // required — pending | completed | failed
25 "lineItems": [
26 // optional; quantity may be negative — a negative
27 { "id": "pli-001", "quantity": -1 } // value means units removed/returned
28 ],
29 "totals": [
30 // optional; amount in minor units (cents) and may be
31 { "type": "total", "amount": -1999, "displayText": "Refund" }, // negative — negative = money
32 { "type": "tax", "amount": -160 } // back to the buyer
33 ],
34 "description": "Customer requested — defective item" // optional
35 }
36}These are the public Shopper endpoints your hooks power. Consumers call them exactly as they would without an OMS integration.
Most of these endpoints belong to the Shopper Orders API, whose base is
/checkout/shopper-orders/v1/organizations/{organizationId}. The one exception is List customer
orders, which is a Shopper Customers API endpoint, with the base
/customer/shopper-customers/v1/organizations/{organizationId}. The API column below notes which
base each path is relative to. siteId is a required query parameter on every endpoint — both
the base and siteId are omitted from the paths for readability.
| Operation | API | Method + path | Request body | Routes to |
|---|---|---|---|---|
| Get order | Shopper Orders | GET /orders/{orderNo}?expand=oms | — | order.retrieve |
| List customer orders | Shopper Customers | GET /customers/{customerId}/orders?expand=oms | — | order.list |
| Guest order lookup | Shopper Orders | POST /orders/{orderNo}/lookup?expand=oms | { orderViewCode?, email?, phone?, postalCode? } | order.retrieve |
| Reason codes | Shopper Orders | GET /orders/oms-meta-data | — | reasonCodes |
| Cancel order | Shopper Orders | POST /orders/{orderNo}/actions/oms-cancel-order | { reason?, orderAccessCode? } | order.cancel |
| Return items | Shopper Orders | POST /orders/{orderNo}/actions/oms-return-order | { productItems: [{ itemId, quantity, reason? }], orderAccessCode? } | order.return |
| Status | When |
|---|---|
| 400 Bad Request | The provided input is not sufficient or valid for the requested action (for example, an unrecognized reason code, an unknown itemId, or a return quantity beyond what’s returnable). |
| 404 Not Found | Order unknown, or the shopper is not authorized to see it (existence is not leaked). |
| 409 Conflict | Order state doesn’t permit the action, your OMS rejected it, or the OMS integration is not active for the site. |
Status. A missing return is treated as failure (surfaces as 404 on
single-order retrieve), not as success.expand=oms on getOrder / getCustomerOrders, or your hook is not called at all and the
platform serves only its own data.retrieve gets no customer id. Don’t implement ownership there — the platform runs access
control for the orders it didn’t already load.customer_info.customer_id is load-bearing. Set it to the platform customer id of the shopper
who owns the order, or the default access check can’t grant access.data
list rather than replacing it, and include only context.customerId’s orders.reasonCodes takes (context, omsMetaData) and its output is cached per site. Return a
static or per-site list; don’t rely on per-request side effects.