Integrating with UBM
UBM is synchronous — there is nothing to subscribe to
🛑 UBM publishes no events for you to consume. Every operation returns its result in the response, including the recalculated basket. If you are looking for a topic to subscribe to in order to learn a basket's price, you are looking for something that does not exist — read the response, or read the basket back.
This is worth stating plainly because most services in this platform do publish a public event stream, so its absence here looks like a gap in the documentation rather than a property of the service. It is a property of the service: UBM was made request/response deliberately, so a channel can show a price in the same round trip that changed the basket.
Downstream consumers exist, but they consume the customer order created at checkout, not the basket. If you need to react to a completed purchase, integrate with the Customer Order service.
Addressing and scope
Every endpoint is business-unit addressed:
/api/v1/business-units/{businessUnitId}/baskets/{basketId}/…
The business unit determines which pricing engine runs, which tender configurations apply and, on the
CHECKOUT engine, which Checkout Engine instance is called. A basket belongs to one business unit for
its whole life. Fulfillments may name a different business unit as their destination — that is how
collection at another store is expressed — but the basket is priced by the business unit in the path.
Endpoints, by the question they answer
Working with the basket itself
| Operation | Endpoint |
|---|---|
| Create or update a basket | PUT /baskets/{basketId} — the basket itself only; see the note below |
| Read a basket, with its calculated result | GET /baskets/{basketId} |
| Find a customer's baskets by loyalty identity | GET /baskets — filtered by loyalty customer id and/or loyalty card number |
| Delete a basket | DELETE /baskets/{basketId} |
| Force a fresh calculation without changing anything | PUT /baskets/{basketId}/recalculate |
| Turn the basket into a customer order | POST /baskets/{basketId}/checkout |
⚠ The only search is by loyalty identity. There is no "list all baskets for this business unit".
Basket ids are supplied by you on PUT, so keep your own reference — and note that a "resume my
basket" flow works only for identified customers, since loyalty identity is the sole lookup key.
🛑 PUT /baskets/{basketId} creates the basket itself, not its contents. The body carries only
currencyId (required), basketType, orderOrigin, basketName and operatorId. It accepts no
items, coupons, loyalty, fulfillments or tenders. There is no single call that submits a whole
basket — though items can now be written as a complete set, see below.
Writing items
| Operation | Endpoint |
|---|---|
| Add or change one line, leaving the others alone | PUT /baskets/{basketId}/items/{itemId} |
| Replace the whole item set | PUT /baskets/{basketId}/items |
| Read the lines | GET /baskets/{basketId}/items |
🛑 These two have opposite failure modes, and they sit on adjacent paths. The bulk PUT treats
your array as the basket's end state, so any line you omit is deleted. Send one item to it and
every other line in the basket disappears — and it succeeds, because that is what you asked for.
Reach for …/items/{itemId} when you mean "change this line".
Use the bulk endpoint when you already hold the whole cart. It diffs against what is stored and touches only what changed: an unchanged line costs no pricing-engine call and no write at all. A 20-line cart re-sent with one quantity edited is a single engine operation. Building the same basket line by line is 20 round trips.
Its rules:
- At most 100 items, matching the basket's own line limit. An oversized array is rejected naming both the limit and the count — never silently truncated.
- Keyed on
itemId, which may appear only once. A duplicate is rejected before anything changes. - An empty array is valid and clears the basket.
- Order is not significant and is not preserved. Do not rely on the order items come back in.
- Only added lines are resolved against the catalogue, and only when a required field is missing — so a storefront that already knows its prices makes no catalogue call at all. Lines already in the basket are never re-resolved.
⚠ Concurrency is improved, not solved. Sending N items in one request removes the window where two of your own parallel single-item calls lose one of the two lines. It does not make two concurrent requests against the same basket safe — one still overwrites the other. Serialise writes per basket.
If you also integrate Customer Order, note the difference: a customer order is submitted complete, in one call. A UBM basket is assembled incrementally and cannot be submitted that way.
Items
| Operation | Endpoint |
|---|---|
| Add or update a line | PUT /baskets/{basketId}/items/{itemId} |
| Read all lines | GET /baskets/{basketId}/items |
| Read one line | GET /baskets/{basketId}/items/{itemId} |
| Remove a line | DELETE /baskets/{basketId}/items/{itemId} |
Coupons
| Operation | Endpoint |
|---|---|
| Apply a coupon | POST /baskets/{basketId}/coupons/{couponId} |
| Read applied coupons | GET /baskets/{basketId}/coupons |
| Remove a coupon | DELETE /baskets/{basketId}/coupons/{couponId} |
Customer and loyalty
| Operation | Endpoint |
|---|---|
| Set or update customer details | POST /baskets/{basketId}/customer |
| Read customer details | GET /baskets/{basketId}/customer |
| Remove customer details | DELETE /baskets/{basketId}/customer |
| Register or update loyalty membership | POST /baskets/{basketId}/loyalty |
| Read loyalty registration | GET /baskets/{basketId}/loyalty |
| Remove loyalty registration | DELETE /baskets/{basketId}/loyalty |
Fulfillments
| Operation | Endpoint |
|---|---|
| Add a fulfillment | POST /baskets/{basketId}/fulfillments |
| Read all fulfillments | GET /baskets/{basketId}/fulfillments |
| Read one fulfillment | GET /baskets/{basketId}/fulfillments/{fulfillmentId} |
| Update a fulfillment | PUT /baskets/{basketId}/fulfillments/{fulfillmentId} |
| Remove a fulfillment | DELETE /baskets/{basketId}/fulfillments/{fulfillmentId} |
Tenders
| Operation | Endpoint |
|---|---|
| Add a tender | POST /baskets/{basketId}/tenders |
| Read all tenders | GET /baskets/{basketId}/tenders |
| Read one tender line | GET /baskets/{basketId}/tenders/{tenderLineId} |
| Update a tender line | PUT /baskets/{basketId}/tenders/{tenderLineId} |
| Remove a tender | DELETE /baskets/{basketId}/tenders/{tenderLineId} |
The authoritative request and response schemas are in the OpenAPI document: https://unified-basket-manager-api.retailsvc.com/schemas/v1/openapi.json
Request semantics worth knowing
Only some calls reprice. A calculation engine is called when something that can change the price changes — not on every write:
| Reprices | Does not reprice |
|---|---|
| Add or update an item | Add, update or remove a fulfillment |
| Remove an item | Set, update or remove the customer |
| Apply or remove a coupon | Add, update or remove a tender |
| Register, update or remove loyalty | |
| Recalculate (explicitly) |
The split follows what the engines actually price: items, coupons and loyalty. A delivery address, a customer's contact details or a means of payment do not change what the goods cost, so they are stored without a round trip.
Two consequences:
-
Cost and latency sit on the repricing calls. Items are the one place you can avoid paying per line:
PUT /itemstakes the whole set and only calls the pricing engine for lines that actually changed. Built line by line instead, a fifty-line basket is fifty calls, each one a pricing-engine round trip — so send the set if you have it.Coupons have no bulk equivalent and are still applied one at a time, each repricing. Fulfillment, customer and tender calls are cheap by comparison, since none of them reprices.
-
⚠ Only repricing calls reset the two-hour price clock. A calculated result older than two hours is rejected at checkout, and the clock restarts on every repricing call — so a basket having items added to it steadily never expires. What does expire is a basket sitting through a long gap of non-repricing work: adding the customer, the fulfillments and the tenders leaves the clock untouched, so a checkout preparation that runs past two hours fails even though you were active throughout. Call recalculate before checkout if there has been a long gap. See Checkout.
Read the total from the calculated result, never compute it. Promotions, coupon outcomes, taxes, fulfillment charges, deposits and fees are all engine output. Checkout compares your tender total against the engine's total, so a locally computed total is the fastest way to fail checkout.
Send the price your storefront displayed. It is used, and it sets the line's regular price — promotions and coupons are then applied on top by the engine, so the line can still cost less than the price you sent. Omit it and the catalogue's price is used instead, which can differ from what the shopper saw. See How pricing works.
Error semantics
| Status | Means |
|---|---|
400 Bad Request | The operation is not valid for this basket's state or configuration — a completed basket, a missing engine configuration, an unresolvable item identifier, or any unmet checkout condition. The message names the specific cause |
404 Not Found | A read addressed something that does not exist — a basket, a tender line — or, on the CHECKOUT engine, the business unit's country is unknown to UBM. ⚠ Note checkout returns 400, not 404, for an unknown basket |
500 | A system UBM depends on failed. The message names it, so a Checkout Engine failure is distinguishable from anything else. Retryable |
⚠ 400 covers both "you sent something wrong" and "this business unit is not provisioned". The
distinction is in the message, and the provisioning case is the one that looks like a code bug when it
is not. The
symptom table in Configuration maps the common messages
to their cause.
Failures upstream of UBM fail your request — they do not produce a half-priced basket. Both of the systems UBM depends on to price a basket behave this way:
- Product master data unavailable → the request fails. UBM will not price from partial catalogue data, because a basket priced against missing item data is worse than no basket.
- The calculation engine failing → the request fails with
500, carrying a message that names the engine, e.g. "Checkout engine post API call failed: …".
So a successful response means the basket was priced by the engine on that call. You never receive a stale price presented as a current one.
Retry is safe for read operations; for mutating operations, read the basket back to establish what actually landed before retrying.