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:

  1. 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 your retrieve hook 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.

  2. 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 retrieve hook (sfcc.app.oms.order.retrieve) supplies this data.

    Third-party OMS only. When your retrieve hook builds the order document, the OMS data it adds (omsData) must reuse the IDs that are already populated in the order, such as order_no, product_items[].item_id, and shipments[].shipment_id. For example, each omsData.shipments[].shipment_items[].product_item_id must match an existing product_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.

  3. 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-events still contains exactly one fulfillmentEvent or adjustment. B2C Commerce adds that entry on top of the order document your retrieve hook 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 retrieve hook 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 fieldSourceRule
versionplatformThe 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 fieldSourceRule
idorder_noCopied as is.
checkout_idplatformThe basket ID of the B2C Commerce order. If B2C Commerce no longer holds the order, oms- followed by order_no.
event_idorder_noevent- followed by order_no.
created_timecreation_dateThe 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_urlplatformThe Order Lookup URL from the site’s UCP channel configuration, used as is.

Each entry in product_items[] becomes one UCP line item.

UCP fieldSourceRule
idproduct_items[].item_idCopied as is. Fulfillment events refer to line items by this ID, so it must be stable.
statusproduct_items[].oms_data.statusprocessing at placement. Changes as the order is fulfilled. See How the UCP Order Grows.
UCP fieldSourceRule
idproduct_items[].product_idCopied as is.
titlecatalog product, else product_items[].product_nameThe 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.
priceproduct_items[].base_priceUnit price in minor units. 0 if not set.
UCP fieldSourceRule
totalproduct_items[].quantity1 if not set.
fulfilledomsData.shipments[]0 at placement. Grows as units ship. See How the UCP Order Grows.
TypeSourceEmitted
subtotalproduct_items[].price (the line total: quantity × unit price)Always
items_discountprice − price_after_item_discountOnly when greater than 0
taxproduct_items[].taxAlways
totalsubtotal − items_discount + taxAlways

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 fieldSourceRule
idshipments[].shipment_idCopied as is.
method_typeplatformAlways shipping.
line_itemsproduct_items[] whose shipment_id matchesOne entry per product item: its item_id and quantity.
descriptionshipments[].gift_messageSet 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 fieldSource
first_namefirst_name
last_namelast_name
street_addressaddress1
extended_addressaddress2
address_localitycity
address_regionstate_code
postal_codepostal_code
address_countrycountry_code
phone_numberphone

At placement, the events log holds a single processing event, which records that the order was placed. It is present in every update.

UCP fieldSource
idorder_no
occurred_atcreation_date
line_itemsThe product items of the order’s shipment: item_id and quantity
TypeSourceEmitted
subtotalproduct_sub_totalAlways
fulfillmentshipping_totalAlways, including 0 (free shipping is reported as 0)
taxadjusted_merchandize_total_tax + adjusted_shipping_total_taxAlways, including 0
items_discountSum over all product items of price − price_after_item_discountOnly when greater than 0
discount(product_sub_total − product_total) − items_discountOnly when greater than 0
totalorder_totalAlways

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 stepfulfillment.events after the updateLine items
Order placedprocessingprocessing, fulfilled 0
Fulfilled (picked and packed)processingprocessing, fulfilled 0
Handed over to the shipping provider and shippedprocessing, shipped (with tracking)fulfilled, fulfilled = ordered
Shopper received the packageprocessing, shipped, deliveredfulfilled, fulfilled = ordered
CompletedNo new event; the last update already shows the complete historyfulfilled, 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 orderSource in the order document
processing eventAlways 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 eventOne 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 eventOne 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.fulfilledThe sum of omsData.shipments[].shipment_items[].quantity per product_item_id.
line_items[].statusproduct_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.

  1. In the event you send to oms-status-events, use the same id as the matching entry in omsData.shipments[]. A shipped event and the shipment it reports then share the same type and id.

  2. Register the hook in your site cartridge’s hooks.json:

  3. In the hook, keep only the first event for each type and id pair. The events built from your retrieve hook 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, or eventId. 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.