How pricing works
When a price-affecting part of the basket changes, UBM assembles the basket, enriches each line from product master data, sends it to a calculation engine, and stores the returned result alongside the basket. The response you get back is that stored result.
Items, coupons and loyalty reprice. Fulfillments, customer and tenders do not — a delivery address, a contact detail or a means of payment does not change what the goods cost. The full split is in Integrating with UBM.
Two engines — and only one of them returns tax
Which engine prices a basket is set per (tenant, business unit). The difference that decides whether an engine is usable is tax, not performance or feature depth:
| Value | Engine | Returns | Character |
|---|---|---|---|
CHECKOUT | Checkout Engine — the engine the point-of-sale itself uses | Price and tax | Stateful, because it was built for a lane: a transaction stays open while items are added one at a time, the way a cashier scans a sale. UBM holds a calculation session per basket and manages it for you. Deployed per tenant and per country |
PROMOTION | Promotion Engine | 🛑 Price only. No tax. The calculated result comes back with an empty set of tax groups | Stateless and multi-tenant. Each call carries the whole basket, so there is no session to hold open or fall out of step |
🛑 PROMOTION returns no tax, and for most channels that is a blocker. It is a real, selectable
configuration, and it will price a basket correctly including promotions — but every tax group on the
calculated result will be empty. It is only viable where the consuming channel does not need tax from
UBM, because it derives or applies tax itself. If your webshop expects UBM to return a
tax-inclusive basket, PROMOTION will not serve it.
⚠ CHECKOUT is what gives the in-store parity guarantee. A basket priced by the Checkout Engine
runs through the same rules as the same basket rung up at a till, tax included. On PROMOTION the
promotions and prices are calculated but the tax half of that comparison is absent, so do not assume
parity without checking which engine a business unit uses — see Configuration.
Because the setting is per business unit, one tenant can run both.
CHECKOUT is the default
A business unit with no engine configuration prices with the Checkout Engine. Nothing has to be provisioned for a new business unit to work, and the same holds if its configuration is later removed — it reverts to the in-store engine rather than stopping.
PROMOTION is therefore always a deliberate choice, made per business unit through customer-controlled
configuration — see Configuration.
Resolving the right engine instance
This applies to the CHECKOUT engine only. It is deployed per tenant and per country, so UBM resolves
the tenant's instance for the country the business unit sits in, with test tenants routed to their
sandbox equivalent.
Three prerequisites follow, and each is silent until it fails:
- The business unit must not be deleted or closed. A basket for one gets
404 Not Foundnaming the tenant and business unit. - The business unit must have a country. UBM keeps its own copy of each business unit's country,
kept up to date from the business-unit events (see Data sources), and asks
Business Unit Management directly when its copy has none. A newly created business unit can
therefore be priced straight away. A business unit with no country in Business Unit Management
cannot be priced on
CHECKOUT: the call returns400 Bad Requestnaming the business unit. - A Checkout Engine instance must be provisioned for that tenant and country. UBM does not create it, and an absent instance surfaces as a failed call rather than as a configuration error.
PROMOTION has none of these prerequisites. It calls a single shared engine and does not need the
country code.
How a line is enriched before pricing
Before a basket reaches either engine, UBM resolves each line against product master data. The rules are worth knowing, because they decide whether the values you send are used:
What you send is used. The catalogue fills in what you omit.
| Field | On CHECKOUT | On PROMOTION |
|---|---|---|
| Item identifier | Yours if you send one; otherwise the catalogue's. Required: if neither exists, the call fails with 400 naming the item | Yours if you send one; otherwise the catalogue's. Not required |
| Regular sales unit price | Yours (unitPrice) is stored on the line but not used for pricing, because the Checkout Engine prices from its own catalogue | Yours if you send unitPrice; otherwise the catalogue's current sales price specification. It prices the line. If neither exists, the call fails with 400 |
| Item name | Yours if you send one; otherwise the name the Checkout Engine returns | Yours if you send one; otherwise the catalogue's |
| Purchase price | Not filled in | The catalogue's current purchase price specification; not settable by the caller |
| Sales unit of measurement | Not filled in | The catalogue's |
Why the identifier matters on one path and not the other: the Checkout Engine identifies items the
way a point of sale does, by the code that would be scanned — a GTIN or PLU. The Promotion Engine
identifies them by item id. So a CHECKOUT business unit needs an identifier for every line, from you
or from the catalogue, while a PROMOTION one does not.
🛑 On a PROMOTION business unit, this is why you should send the price the shopper was shown. If
your storefront displayed 110.00 and the catalogue holds 120.00 — because the two are momentarily out
of step — sending 110.00 as unitPrice is what keeps the basket agreeing with the page the shopper
clicked on. Omit it and the basket prices at 120.00. On a CHECKOUT business unit the Checkout Engine
prices from its own catalogue whatever you send.
The price you send sets the regular price, not the final one
A line carries two prices in the calculated result, and conflating them is the usual source of "the price I sent is not the price I got back":
| Field | Meaning |
|---|---|
regularSalesUnitPrice | the baseline — the undiscounted price the line starts from |
actualSalesUnitPrice | what the line actually costs after the engine has applied promotions, coupons and loyalty |
So a supplied price feeds the baseline, and a promotion can still take the line below it. Both of these are true at once: your price was used, and the line costs less than your price. The difference between the two, times quantity, is the line's discount.
For the line's total, read extendedAmount rather than multiplying actualSalesUnitPrice by the
quantity. The unit price is rounded, so the product can be a cent out.
Discounts are not something you send. They come from promotions and coupons, which the engine applies.
A price specification counts only when it is the current specification of the right type. A price that is superseded, or of another type, is ignored rather than treated as a fallback.
A stored price expires after two hours
The calculated result is stored with the timestamp of the operation that produced it, and checkout rejects a result older than two hours:
"Basket price is not calculated for more than 2 hours. Please recalculate the basket."
Call recalculate to refresh it. Recalculate also exists for the case where nothing about the basket changed but the catalogue or the promotions did — it forces a fresh calculation without altering the basket.
⚠ This matters for saved baskets. A basket a customer left overnight will price fine on read but fail at checkout until it is recalculated. Recalculating can legitimately change the total, so show the refreshed price rather than the one the customer last saw.
⚠ Omitting the price hands the decision to the catalogue
On a PROMOTION business unit, if you do not send a price, the catalogue's is used — and if your
storefront and the catalogue have drifted, the basket will not match what the shopper saw. Nothing detects this: both systems are
internally consistent, and UBM has no third opinion to compare against.
The practical rule: send the price you displayed. Falling back to the catalogue is right for items your channel does not price, and a silent mismatch for items it does.