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.
your request product master data (PnP)
│ │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ Unified Basket Manager │
│ 1. apply the change to the input basket │
│ 2. enrich lines: name, identifier, prices, UoM │
│ 3. price it with this business unit's engine │
└───────────────┬──────────────────────┬───────────────┘
│ │
CHECKOUT│ │PROMOTION
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ Checkout Engine │ │ Promotion Engine │
│ price AND tax │ │ price only — │
│ per tenant and │ │ NO tax │
│ country, stateful │ │ stateless │
└─────────┬──────────┘ └─────────┬──────────┘
└───────────┬────────────┘
▼
calculated result, stored
and returned to you
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.
Two prerequisites follow, and both are silent until they fail:
- The business unit's country must be known to UBM. UBM keeps its own cache of business-unit
country codes, fed from the business-unit event stream (see Data sources). A
business unit whose country has never arrived produces a
404 Not Foundnaming the tenant and 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 neither prerequisite — 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 | Resolution |
|---|---|
| Item identifier | Yours if you send one; otherwise the catalogue's. Required on the CHECKOUT path only — if neither exists there, the call fails with 400 naming the item |
| Regular sales unit price | Yours if you send one; otherwise the catalogue's current active sales price specification |
| Purchase price | Yours if you send one; otherwise the catalogue's current active purchase price specification |
| Item name | Yours if you send one; otherwise the catalogue's |
| Sales unit of measurement | Yours if you send one; otherwise 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.
🛑 This is why you should send the price the shopper was shown. If your storefront displayed 80 and the catalogue holds 90 — because the two are momentarily out of step — sending 80 is what keeps the basket agreeing with the page the shopper clicked on. Omit it and the basket prices at 90.
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 what the basket reports as its discount.
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, active specification of the right type. A price that is inactive, 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
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.