The basket model
Two halves: what you sent, and what it costs
The single most useful thing to understand about UBM is that a basket has two distinct halves, and the API exposes both:
| Half | What it is | Who writes it |
|---|---|---|
| The input basket | What you asked for: items and quantities, coupons applied, the loyalty membership, the customer, fulfillments, tenders | You, through the endpoints |
| The calculated result | What it costs: priced lines, unit discounts, the promotions that fired, coupon outcomes, loyalty outcomes, and tax groups | The calculation engine, whenever a price-affecting part of the basket changes |
🛑 Tax groups are empty on business units configured for the PROMOTION engine — it returns price
without tax. See How pricing works.
You never write the calculated result, and you should never derive one yourself. If a total looks wrong, the question is which engine produced it and from what input — not whether UBM added it up correctly.
The calculated result lists the coupons the engine accepted, and the promotions and discounts that actually applied. A coupon that is in the input basket but missing from the calculated result was not accepted, for example because it has expired, and took nothing off. That is the signal to look for, rather than assuming a coupon in the basket means a discount was given. What an accepted coupon took off shows on the lines, as their promotions and actual price.
Status
| Status | Meaning |
|---|---|
ACTIVE | The basket can be read and changed. Every basket starts here |
COMPLETED | The basket has been checked out and a customer order exists |
🛑 A COMPLETED basket is locked, not merely discouraged. Every modifying operation — items,
coupons, customer, loyalty, fulfillments, tenders, recalculate, a further checkout — returns
400 Bad Request saying the basket is completed: "Cannot modify a completed basket" or "Basket is
COMPLETED or does not exist and cannot be modified.". Every read still works, but the basket can never
be revived. To sell the same goods again, create a new basket.
A completed basket cannot be deleted either ("Basket is COMPLETED or does not exist and cannot be deleted."). It is the record that ties the purchase to its customer order, and it is removed automatically, like any other basket — see How long a basket lives.
There is no cancelled or expired status. A basket you no longer want is deleted.
How long a basket lives
A basket is not automatically expired or cleaned up. There is no abandoned-cart sweep and no
tenant setting that removes baskets, so an unfinished basket stays ACTIVE and fully usable until you
delete it.
Storage is reclaimed only after 18 months without modification, and that period is sliding — most calls restart it. A cart a shopper returns to every few weeks therefore persists indefinitely, and a long-lived webshop basket never needs recreating.
Checkout does not restart the period, so a completed basket is removed 18 months after the last change made to it before checkout.
⚠ Treat 18 months as the retention period today, not a guarantee to build against. It may change. If your integration depends on baskets surviving a particular length of time, tell us rather than inferring it from this number.
What restarts each clock
Two independent clocks run on a basket, and the same call does not necessarily restart both. Retention restarts when a call writes the basket record itself; the two-hour price clock restarts when a call produces a new calculated price. Creating or replacing the basket restarts retention; beyond that:
| Call | Restarts 18-month retention | Resets the two-hour price clock |
|---|---|---|
| Recalculate | yes | yes |
| Add, change or remove an item | yes | yes |
| Add a coupon | yes | yes |
| Remove a coupon | yes | yes |
| Add or change loyalty | yes | yes |
| Remove loyalty | yes | yes |
| Set or clear the customer | yes | no |
| Any fulfillment call | no | no |
| Any tender call | no | no |
The rows that restart neither are narrow: a basket would have to sit untouched for 18 months except for those calls before retention were affected. For a cart being built or revisited, every call a storefront normally makes restarts it.
🛑 A long-lived basket still meets the two-hour price clock. The basket survives; its calculated price does not. A shopper returning to an old cart can read it, but checkout fails until you recalculate — and the total can legitimately have changed in the meantime. See Checkout.
Sub-resources
Each of these is separately addressable. Only items, coupons and loyalty reprice the basket — fulfillments, customer and tenders are stored without calling an engine, because none of them changes what the goods cost.
Items
Lines carry the item, quantity, unit of measure, and — after calculation — the resolved name, identifier, regular and actual unit prices, tax group, and any deposit or fee rules that apply. Quantity is fractional, so weight- and measure-based items work without a separate mechanism.
There are two ways to write lines, and they behave very differently:
- One line at a time —
PUT …/items/{itemId}changes that line and leaves the rest alone. - The whole set at once —
PUT …/itemstakes the complete item set the basket should hold.
🛑 The bulk call is a replace, not an append. The array you send is the basket afterwards: an item you leave out is deleted, and an empty array clears the basket. It is the desired end state, not a list of changes.
Prefer it when you hold the whole cart anyway — a storefront usually does. It is also markedly faster, because it works out what actually changed and only touches those lines: re-sending a 20-line cart with one quantity edited costs a single engine call, not twenty.
⚠ Quantity is replaced, never accumulated — on both endpoints. Sending quantity: 3 for a line
holding 5 leaves it at 3, not 8. Worth pausing on, because "add to cart" in a storefront usually
means increment.
The basket-level PUT still carries no lines. See
Integrating with UBM.
Resolution of names, identifiers and prices against the catalogue is described in How pricing works. The short version: what you send is used, and the catalogue fills in what you omit — so send the price your storefront displayed.
Coupons
Applied and removed individually, and each reprices. Whether a coupon actually discounts anything is decided by the engine and visible in the calculated result, not in the input basket.
Loyalty
The loyalty membership the basket is priced for. Registering loyalty can change prices, so it reprices; it is not merely an attribute recorded for later.
Customer
Who is buying. Held separately from loyalty, because a basket can have a customer without a membership and a membership without full customer details. Does not reprice — customer details do not affect price; loyalty membership is what can.
Fulfillments
How the basket reaches the customer. A basket can have several fulfillments — for example part collected and part delivered — and each can carry its own destination business unit and its own line items. Does not reprice. Fulfillments matter at checkout, where tenders are allocated to them.
Tenders
The means of payment, with authorization data where the tender type needs it (card and gift-card
authorizations are held separately). Card and gift-card tenders must carry authorization data, and
other tender types must not; a tender that breaks this is refused with 400 when it is added.
Does not reprice. Tenders are validated at checkout against the business
unit's active tender configurations, and where the basket also has fulfillments their total must
match the order total — see Checkout.
External identifiers
A basket and its fulfillments can carry external identifiers — your own references, kept alongside UBM's ids so you can correlate a basket with an order in your own systems. UBM stores and returns them without interpreting them.
Additional properties
Baskets accept additional properties for data the model does not have a field for. They are carried through and returned, not interpreted, and they do not influence pricing.