Checkout
Checkout converts a priced basket into a customer order. It is a single synchronous call that either creates the order and completes the basket, or fails and changes nothing about the basket's status.
What must be true before checkout succeeds
Every one of these returns 400 Bad Request — including the missing-basket case, which is worth
noting because reading a non-existent basket returns 404 instead. Checkout treats an unknown
basket as a bad request rather than a missing resource.
| # | Condition | If it fails |
|---|---|---|
| 1 | The basket exists | Basket not found — 400 here, unlike a GET |
| 2 | The basket is not already COMPLETED | Cannot modify a completed basket |
| 3 | A calculated result exists for it | Basket not found or not calculated — a basket that has never been priced cannot be checked out |
| 4 | The calculated result is less than two hours old | Basket price is not calculated for more than 2 hours. Please recalculate the basket. |
| 5 | The calculated result has at least one line | Basket must contain at least one item before checkout |
| 6 | If tenders are present, active tender configurations exist for the business unit | No active tender configurations found |
| 7 | If tenders are present, every tender's id is among the active configurations | Non-active tender IDs: …, listing them |
| 8 | Every fulfillment's payment satisfies the rule for its method — see below | Fulfillment F must be either unpaid or fully paid for its method, but X of Y is allocated to it, or Fulfillment F requires full payment for its method but only X of Y is allocated to it, or Fulfillment F is allocated X, more than its share of the order (Y) |
| 9 | Each tender's allocations across fulfillments add up to what it paid | Tender T is allocated X across fulfillments, less than the Y it paid — or more than, if they exceed it |
| 10 | If tenders and fulfillments are both present, tenders can be allocated to fulfillments | Tender T must be allocated to a fulfillment when multiple fulfillments exist — see below |
| 11 | If tenders are present and there are no fulfillments, the tenders do not exceed the order total | Total tender amount (X) exceeds total order amount (Y) |
How much must be paid depends on the fulfillment method
A collection order can be completed unpaid or part-paid, because the point of sale takes the balance when the customer arrives. A delivery order is either unpaid — the point of sale takes the whole amount afterwards — or paid in full by a webshop before checkout. A part-paid delivery order has no flow behind it and is refused while the caller can still act on it.
The rule is applied per fulfillment, against the tenders allocated to that fulfillment and its own share of the order — so "its share" below means the whole order total for a single-fulfillment basket, and that fulfillment's portion of it otherwise.
| Tenders allocated to the fulfillment | PICKUP_IN_STORE | HOME_DELIVERY_FROM_WAREHOUSE · HOME_DELIVERY_FROM_STORE |
|---|---|---|
| None | ✅ Accepted — the store collects the full amount on collection | ✅ Accepted — the point of sale takes the whole amount afterwards |
| Less than its share | ✅ Accepted — the store collects the balance | 🛑 Rejected — a delivery order is unpaid or fully paid, never part-paid |
| Exactly its share | ✅ Accepted | ✅ Accepted |
| More than its share | 🛑 Rejected | 🛑 Rejected |
Over-payment is refused for every method. UBM will not create an order that has been paid more than it is worth; correct the tender amounts instead.
⚠ A shortfall on one fulfillment is never offset by a surplus on another. A basket that is part collected and part delivered must have the delivery portion either unpaid or fully covered by the tenders allocated to that fulfillment, while the collection portion may be left short — and it is refused if the delivery side is part-paid, however much the basket has taken in total. This is why a multi-fulfillment basket has to allocate its tenders explicitly — see below.
A fulfillment's share is derived from the quantities allocated to it: each line's unit amount, taken from the calculated result, times the quantity that fulfillment carries, with the order total divided across the fulfillments so that the shares add up to it exactly. You do not supply it, and there is no field for it on the fulfillment.
A partially authorized tender counts for what was authorized, not what was requested. Where a tender carries authorization data, its authorized amount is what UBM treats as paid — including an authorization of zero, which is a refusal and counts as nothing. A tender without authorization data counts for its stated amount.
💡 If you need the shopper to pay in full regardless, enforce that in your own flow before calling checkout. For collection, UBM's rule is a floor on what it will accept, not a statement about what your channel should require. For delivery there is no floor to relax against — UBM accepts exactly two points, unpaid or paid in full, so a channel that wants payment up front still has to enforce it itself.
The two-hour expiry
The calculated result carries the timestamp of the operation that produced it, and checkout rejects one older than two hours. Call recalculate and retry.
⚠ Recalculating can change the total, so re-read it before adding or adjusting tenders — and show the customer the refreshed price rather than the one they last saw.
A result with no timestamp is treated as not expired.
⚠ The clock is reset only by repricing calls — items, coupons, loyalty and recalculate. Adding the customer, fulfillments or tenders does not touch it, so a basket can expire during checkout preparation despite continuous activity. Re-sending an unchanged item set does not count either: nothing is repriced, so the clock is not reset.
Tender allocation to fulfillments
Where both tenders and fulfillments exist, tenders are allocated to the fulfillments they pay for:
| Situation | Outcome |
|---|---|
| All tenders already allocated | Used as given |
| Unallocated tenders, and exactly one fulfillment | UBM allocates them all to that fulfillment automatically |
| Unallocated tenders, and more than one fulfillment | 🛑 400. UBM will not guess which fulfillment a payment belongs to. Allocate them explicitly |
Where amounts are allocated, a tender's allocations across all fulfillments must add up to what it paid — its authorized amount where it carries authorization data, its stated amount otherwise. A mismatch either way is refused rather than reconciled: allocating more than a tender paid spends money the provider never approved, and allocating less leaves money the basket has taken unassigned.
A tender may be split across fulfillments, so a single payment covering both a collection and a delivery portion is allocated in two parts that add up to what it paid.
🛑 A multi-fulfillment basket needs explicit tender allocation. This is the failure most likely to appear only in production, because a single-fulfillment basket works without you ever allocating anything — so the requirement is invisible until a customer splits collection and delivery.
A concurrent change refuses checkout, and creates no order
Checkout reads the basket, then asks Customer Order to create the order, then marks the basket
COMPLETED. If another request changes the basket between that first read and the Customer Order
call, checkout is refused with 409 Conflict naming the basket — and this request created no
customer order. The refusal happens before Customer Order is called, so the 409 itself never
leaves an order behind.
Re-read the basket before you do anything else:
- Still
ACTIVE: nothing was ordered. Check out again. COMPLETED, with acustomerOrderId: the conflicting change was a checkout of this basket, typically your own retry or a double-submit, and that one created the order. Use thatcustomerOrderId; do not check out again.
| When the change lands | What you get |
|---|---|
| Before checkout's first read | Checkout completes normally, with the change included |
| Between that read and the Customer Order call | 🛑 409 Conflict. This request created no order. Re-read the basket and act on its status, as above |
| While the Customer Order call is in flight | Checkout returns 200 and completes the basket. The change is not in the order, although its own call returned 200. Serialise writes per basket, and do not change a basket while its checkout is running |
| After the order exists | Checkout never returns 409 at this point. The order is real, so the basket records it and returns 200 |
⚠ A 409 from checkout means this call created no order — not that no order exists. If two
checkouts of one basket can overlap (a double-submit, or a retry after a timeout), the re-read above
is how you tell which case you are in.
After checkout
The customer order is created first, then the basket is marked COMPLETED. From that point:
- The basket is read-only: it cannot be changed, revived or deleted. Reads still work, and it is removed automatically 18 months after its last change.
- The customer order is the durable record. Its lifecycle — fulfillment, payment capture, cancellation — belongs to the Customer Order service, not to UBM.
- The customer order id is associated with the basket, so you can correlate the two afterwards.
- The order can gain lines the basket never had. When a store picks the order and replaces an item with a substitute, Customer Order adds the substitute as a new line. The basket is not updated, so after picking, the basket and the order's lines no longer match. Read lines from the customer order, not from the basket. See Substitutions add order lines.
⚠ Do not treat a successful checkout as a stock reservation. UBM never reserved the goods. Whether the order can be fulfilled is decided downstream.
Before you call checkout
The conditions above are cheap to satisfy in the right order, and expensive to debug in the wrong one. A reliable sequence:
- Add every item, coupon and loyalty membership first — these are the calls that reprice.
- Add the customer and the fulfillments. Neither reprices, so their order does not matter.
- Read the calculated result and take the order total from it.
- Add tenders, allocating them to fulfillments if there is more than one. Never allocate a fulfillment more than its share; a delivery fulfillment must have all of it or none of it, and a collection fulfillment may have less or none.
- Call checkout. ⚠ Steps 2 to 4 do not reprice, so they do not reset the two-hour price clock — if they took a while, recalculate before this step and re-read the total.
Taking the total from the calculated result rather than computing it yourself is what keeps condition 8 satisfied. The total is the engine's output, so it already accounts for promotions, coupon outcomes and tax — none of which you can reliably reproduce.