How the UCP Order Is Built from the Shopper Order Document
This page explains how B2C Commerce builds the UCP order in each post-processing update from the shopper order document, and which order-document fields fill each part of the payload.
When a shopper places an order through an agentic-commerce channel such as Google Gemini, B2C Commerce creates the order and sends the first update to the agent platform. From then on, your Order Management System (OMS) is the system of record: fulfillment, shipping, delivery, cancellation, and returns happen there. The agent platform keeps its own copy of the order to show to the shopper.
B2C Commerce keeps that copy current. Whenever the order changes in your OMS, B2C Commerce takes the order data from your OMS, turns it into the format the agent platform expects, and delivers it as a complete UCP order carrying a cryptographic signature. Your OMS integration only connects to B2C Commerce. Everything that’s specific to the agent platform is handled for you.
How post-processing events reach B2C Commerce, from Salesforce OMS or from a third-party OMS, is described in Order Post-Processing Events in the UCP Order Lifecycle. This page explains what goes into the UCP order of each update.
UCP requires each update to contain the complete, current state of the order, not an incremental delta. It contains what was bought, at what price, where it ships, and the order’s full history so far. The agent platform replaces its copy with each update it receives, so anything the update leaves out is also missing from what the shopper sees. As your OMS processes the order, the UCP order grows by the latest fulfillment step.
This has three consequences for how B2C Commerce builds the payload:
-
The UCP order is rebuilt for every update. B2C Commerce doesn’t store a UCP version of the order or patch a previous payload. For each post-processing event, it builds a fresh UCP order from the current state of the order.
Third-party OMS only. B2C Commerce never stores a history of the events your third-party OMS sends to
oms-status-events. The order document that yourretrievehook returns is the source of truth for each UCP order. Any step that belongs in the agent’s view of the order, now or in a later update, must be part of that order document. -
The current state lives in the shopper order document. The shopper order document is the order shape that Shopper Orders API returns: line items, prices, shipments, totals, and the live fulfillment data under
omsData. Building the UCP order from this one shape keeps the payload identical regardless of which system manages the order.With Salesforce OMS, B2C Commerce fetches the order data automatically, including all the data this page describes. There is no retrieve hook to build. Model each delivery with a Shipment and its ShipmentItem records so that document includes fulfillment data. See Fulfillment Data Mapping. With a third-party OMS, your
retrievehook (sfcc.app.oms.order.retrieve) supplies this data.Third-party OMS only. When your
retrievehook builds the order document, the OMS data it adds (omsData) must reuse the IDs that are already populated in the order, such asorder_no,product_items[].item_id, andshipments[].shipment_id. For example, eachomsData.shipments[].shipment_items[].product_item_idmust match an existingproduct_items[].item_id. This keeps the order graph consistent. If the IDs don’t match, fulfillment events and quantities can’t be linked to the order’s line items.This page currently covers fulfillment only: shipping and delivery. Returns and cancellations (the
adjustments[]part of the UCP order) can’t be built from the order document yet, because the order document has no model for them. As a result, each update carries at most one adjustment: the return or cancellation that triggered the update. Earlier adjustments aren’t repeated in later updates. This applies to both Salesforce OMS and a third-party OMS.This is a temporary limitation. A future release will add a return and cancellation model to the order document so that B2C Commerce can build the complete adjustment history from it, the same way it builds fulfillment events.
-
Salesforce OMS sends a signal, and a third-party OMS still posts the event. With Salesforce OMS, B2C Commerce receives an event for specific updates, then fetches the current order document. Which updates send an event is described in UCP Order Updates from Salesforce Order Management. With a third-party OMS, each call to
oms-status-eventsstill contains exactly onefulfillmentEventoradjustment. B2C Commerce adds that entry on top of the order document yourretrievehook returns. How to remove a step that then appears twice is described in Removing Duplicate Events.A later signal-only model is planned for third-party OMS calls. In that model, the call carries no order data, and B2C Commerce builds the UCP order only from what your
retrievehook returns. Build that hook to return the complete, current state of the order, as described on this page. When the call becomes a signal, you change only the call.In both cases, B2C Commerce translates the whole document into a UCP order and delivers it with a cryptographic signature.
The quality of every update therefore depends on the order document. A field that is missing or wrong in the order document is missing or wrong in the agent’s view of the order. The rest of this page shows what a UCP order looks like when the order is placed, which order-document field fills each part of it, and how it grows as the OMS processes the order.
This order has two line items in one shipment. It has just been placed, so nothing has been fulfilled yet. The currency is USD. Amounts are in minor units (cents).
The next sections go through this payload one part at a time.
In the tables, Source names the order-document field as your hook sets it (snake_case). Values marked platform are supplied by B2C Commerce, not by the order document.
Monetary amounts are converted to the currency’s minor units: ×100 for USD and EUR, ×1 for JPY, ×1000 for KWD. Fractions below one minor unit are truncated.
| UCP field | Source | Rule |
|---|---|---|
version | platform | The UCP version the agent platform declared when the order was placed, if B2C Commerce still holds the order and supports that version. Otherwise, the platform’s configured default version. |
The version also determines whether discount amounts are negative or positive. See line_items[].totals and totals.
| UCP field | Source | Rule |
|---|---|---|
id | order_no | Copied as is. |
checkout_id | platform | The basket ID of the B2C Commerce order. If B2C Commerce no longer holds the order, oms- followed by order_no. |
event_id | order_no | event- followed by order_no. |
created_time | creation_date | The order’s creation time. If creation_date isn’t set, then B2C Commerce uses the time that the UCP order is built, so always set creation_date. |
permalink_url | platform | The Order Lookup URL from the site’s UCP channel configuration, used as is. |
Each entry in product_items[] becomes one UCP line item.
| UCP field | Source | Rule |
|---|---|---|
id | product_items[].item_id | Copied as is. Fulfillment events refer to line items by this ID, so it must be stable. |
status | product_items[].oms_data.status | processing at placement. Changes as the order is fulfilled. See How the UCP Order Grows. |
| UCP field | Source | Rule |
|---|---|---|
id | product_items[].product_id | Copied as is. |
title | catalog product, else product_items[].product_name | The name of the catalog product with that ID, in the shopper’s checkout locale when B2C Commerce still holds the order, otherwise in the site’s default locale. If the product isn’t in the site catalog, product_name is used. |
price | product_items[].base_price | Unit price in minor units. 0 if not set. |
| UCP field | Source | Rule |
|---|---|---|
total | product_items[].quantity | 1 if not set. |
fulfilled | omsData.shipments[] | 0 at placement. Grows as units ship. See How the UCP Order Grows. |
| Type | Source | Emitted |
|---|---|---|
subtotal | product_items[].price (the line total: quantity × unit price) | Always |
items_discount | price − price_after_item_discount | Only when greater than 0 |
tax | product_items[].tax | Always |
total | subtotal − items_discount + tax | Always |
In UCP version 2026-04-08 and later, B2C Commerce sends items_discount as a negative value. In earlier versions, it’s positive.
Each entry in shipments[] becomes one expectation.
| UCP field | Source | Rule |
|---|---|---|
id | shipments[].shipment_id | Copied as is. |
method_type | platform | Always shipping. |
line_items | product_items[] whose shipment_id matches | One entry per product item: its item_id and quantity. |
description | shipments[].gift_message | Set only when the shipment is a gift (gift is true) and has a message. |
destination is mapped from shipments[].shipping_address. If the shipment has no shipping address, an empty address is sent.
| UCP field | Source |
|---|---|
first_name | first_name |
last_name | last_name |
street_address | address1 |
extended_address | address2 |
address_locality | city |
address_region | state_code |
postal_code | postal_code |
address_country | country_code |
phone_number | phone |
At placement, the events log holds a single processing event, which records that the order was placed. It is present in every update.
| UCP field | Source |
|---|---|
id | order_no |
occurred_at | creation_date |
line_items | The product items of the order’s shipment: item_id and quantity |
| Type | Source | Emitted |
|---|---|---|
subtotal | product_sub_total | Always |
fulfillment | shipping_total | Always, including 0 (free shipping is reported as 0) |
tax | adjusted_merchandize_total_tax + adjusted_shipping_total_tax | Always, including 0 |
items_discount | Sum over all product items of price − price_after_item_discount | Only when greater than 0 |
discount | (product_sub_total − product_total) − items_discount | Only when greater than 0 |
total | order_total | Always |
In UCP version 2026-04-08 and later, B2C Commerce sends items_discount and discount as negative values. In earlier versions, they’re positive. All other totals are non-negative.
After placement, an order usually moves through the OMS the same way. It is fulfilled (picked and packed), handed over to the shipping provider, and shipped. The shopper receives the package, and the order is complete. After each step, the OMS signals B2C Commerce, which sends a new update. What was ordered and the totals stay the same. The fulfillment events log grows and the line items’ fulfillment state advances.
| OMS step | fulfillment.events after the update | Line items |
|---|---|---|
| Order placed | processing | processing, fulfilled 0 |
| Fulfilled (picked and packed) | processing | processing, fulfilled 0 |
| Handed over to the shipping provider and shipped | processing, shipped (with tracking) | fulfilled, fulfilled = ordered |
| Shopper received the package | processing, shipped, delivered | fulfilled, fulfilled = ordered |
| Completed | No new event; the last update already shows the complete history | fulfilled, fulfilled = ordered |
For Salesforce OMS, the delivered event isn’t available yet. See Shipment Completed.
After delivery, the example order carries this events log, and both lines show "status": "fulfilled" with fulfilled equal to total:
Each UCP order is rebuilt from scratch. See Why the UCP Order Is Built from the Shopper Order. The history comes from the order document every time. Salesforce OMS supplies these fields automatically. A third-party retrieve hook returns them:
| Data in the UCP order | Source in the order document |
|---|---|
processing event | Always present, one per order. occurred_at is the order’s creation date (creation_date on the order). id is order_no. line_items are the order’s product items (item_id, quantity). |
shipped event | One per entry in omsData.shipments[]. occurred_at is the shipment’s creation date (omsData.shipments[].created_date), the moment the OMS created the shipment and handed it to the carrier. id is omsData.shipments[].id. line_items come from shipment_items[] (product_item_id → id, quantity). Tracking comes from tracking_number, tracking_url, and provider (→ carrier). |
delivered event | One per entry in omsData.shipments[] that has an actual_delivery_date. occurred_at is the shipment’s actual delivery date (omsData.shipments[].actual_delivery_date). id, line_items, and tracking are the same as for the shipped event of that shipment. |
line_items[].quantity.fulfilled | The sum of omsData.shipments[].shipment_items[].quantity per product_item_id. |
line_items[].status | product_items[].oms_data.status: FULFILLED → fulfilled, PARTIALLY_FULFILLED → partial, anything else → processing. |
With Salesforce OMS, B2C Commerce reads this document when one of the updates occurs. With a third-party OMS, update the state your retrieve hook reads before you call oms-status-events. Otherwise the update shows the previous state. The hook must return the complete history every time. A shipment that the hook no longer returns disappears from the agent’s view.
Until the signal-only model is available, the event your OMS sends to oms-status-events is added to the UCP order in addition to the events built from your retrieve hook. If both describe the same step, the UCP order lists it twice.
Remove these duplicates with the sfcc.ucp.orderWebhook.v1.modifyEventPayload hook. B2C Commerce calls this hook with the finished UCP order right before the order is signed and delivered. Change the order in place. See Business Profile and Order Webhooks.
The payload argument is a script object. Read and write camelCase properties, such as checkoutId, eventId, and permalinkUrl. B2C Commerce serializes the order to snake_case when it sends the order. The JSON samples on this page show that sent format.
-
In the event you send to
oms-status-events, use the sameidas the matching entry inomsData.shipments[]. Ashippedevent and the shipment it reports then share the sametypeandid. -
Register the hook in your site cartridge’s
hooks.json: -
In the hook, keep only the first event for each
typeandidpair. The events built from yourretrievehook come first, so that entry is kept and the duplicate that was sent is removed.
Keep these rules for the hook in mind:
- Always return
Status.OK. If the hook throws or returns an error, the update isn’t sent and is retried later. - Don’t change
id,checkoutId, oreventId. If any of them changes, the update isn’t delivered. - Keep it fast. The hook runs under a time limit. Don’t call your OMS from it.
Moving to the signal-only model. This filter also supports the switch. After oms-status-events becomes a signal, no event is added to the UCP order, so there are no duplicates. The filter then finds nothing to remove and changes nothing. Your integration keeps working while you change the call. Remove the hook afterwards.