Skip to main content

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

OperationEndpoint
Create or update a basketPUT /baskets/{basketId} — the basket itself only; see the note below
Read a basket, with its calculated resultGET /baskets/{basketId}
Find a customer's baskets by loyalty identityGET /baskets — filtered by loyalty customer id and/or loyalty card number
Delete a basketDELETE /baskets/{basketId}
Force a fresh calculation without changing anythingPUT /baskets/{basketId}/recalculate
Turn the basket into a customer orderPOST /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

OperationEndpoint
Add or change one line, leaving the others alonePUT /baskets/{basketId}/items/{itemId}
Replace the whole item setPUT /baskets/{basketId}/items
Read the linesGET /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

OperationEndpoint
Add or update a linePUT /baskets/{basketId}/items/{itemId}
Read all linesGET /baskets/{basketId}/items
Read one lineGET /baskets/{basketId}/items/{itemId}
Remove a lineDELETE /baskets/{basketId}/items/{itemId}

Coupons

OperationEndpoint
Apply a couponPOST /baskets/{basketId}/coupons/{couponId}
Read applied couponsGET /baskets/{basketId}/coupons
Remove a couponDELETE /baskets/{basketId}/coupons/{couponId}

Customer and loyalty

OperationEndpoint
Set or update customer detailsPOST /baskets/{basketId}/customer
Read customer detailsGET /baskets/{basketId}/customer
Remove customer detailsDELETE /baskets/{basketId}/customer
Register or update loyalty membershipPOST /baskets/{basketId}/loyalty
Read loyalty registrationGET /baskets/{basketId}/loyalty
Remove loyalty registrationDELETE /baskets/{basketId}/loyalty

Fulfillments

OperationEndpoint
Add a fulfillmentPOST /baskets/{basketId}/fulfillments
Read all fulfillmentsGET /baskets/{basketId}/fulfillments
Read one fulfillmentGET /baskets/{basketId}/fulfillments/{fulfillmentId}
Update a fulfillmentPUT /baskets/{basketId}/fulfillments/{fulfillmentId}
Remove a fulfillmentDELETE /baskets/{basketId}/fulfillments/{fulfillmentId}

Tenders

OperationEndpoint
Add a tenderPOST /baskets/{basketId}/tenders
Read all tendersGET /baskets/{basketId}/tenders
Read one tender lineGET /baskets/{basketId}/tenders/{tenderLineId}
Update a tender linePUT /baskets/{basketId}/tenders/{tenderLineId}
Remove a tenderDELETE /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:

RepricesDoes not reprice
Add or update an itemAdd, update or remove a fulfillment
Remove an itemSet, update or remove the customer
Apply or remove a couponAdd, 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 /items takes 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

StatusMeans
400 Bad RequestThe 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 FoundA 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
500A 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.