Skip to main content

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.

Checkout runs five groups of checks: the basket exists and is not completed; it was priced under two hours ago with at least one line; every tender is an active tender configuration; each fulfillment is paid as its method allows; and each tender's allocations add up to what it paid. If any fails, the call returns 400 Bad Request naming the reason and the basket stays ACTIVE. If all pass, the customer order is created first and the basket is then marked COMPLETED.

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.

#ConditionIf it fails
1The basket existsBasket not found — 400 here, unlike a GET
2The basket is not already COMPLETEDCannot modify a completed basket
3A calculated result exists for itBasket not found or not calculated — a basket that has never been priced cannot be checked out
4The calculated result is less than two hours oldBasket price is not calculated for more than 2 hours. Please recalculate the basket.
5The calculated result has at least one lineBasket must contain at least one item before checkout
6If tenders are present, active tender configurations exist for the business unitNo active tender configurations found
7If tenders are present, every tender's id is among the active configurationsNon-active tender IDs: …, listing them
8Every fulfillment's payment satisfies the rule for its method — see belowFulfillment 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)
9Each tender's allocations across fulfillments add up to what it paidTender T is allocated X across fulfillments, less than the Y it paid — or more than, if they exceed it
10If tenders and fulfillments are both present, tenders can be allocated to fulfillmentsTender T must be allocated to a fulfillment when multiple fulfillments exist — see below
11If tenders are present and there are no fulfillments, the tenders do not exceed the order totalTotal 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 fulfillmentPICKUP_IN_STOREHOME_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.

How much may already be paid depends on the fulfillment method. A collection with a share of 160.00 is accepted with nothing, part or all of it paid, and the store takes the rest. A home delivery with a share of 120.00 is accepted with nothing or all of it paid, but never part-paid. Paying more than the share is refused for both.

⚠ 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.

Each fulfillment is checked on its own. A card payment of 200.00 split as 140.00 to the collection and 60.00 to the 120.00 delivery is refused, because the delivery is part-paid, although 200.00 was paid in total. Split as 80.00 and 120.00 it is accepted, and the store takes the rest of the collection.

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.

The two-hour clock runs from the last call that repriced the basket. The last item is added at 10:00; adding the customer at 10:30, the fulfillments at 11:10 and the tenders at 11:50 does not reprice, so checkout at 12:20 is refused. Recalculate, re-read the total, then check out.

Tender allocation to fulfillments​

Where both tenders and fulfillments exist, tenders are allocated to the fulfillments they pay for:

SituationOutcome
All tenders already allocatedUsed as given
Unallocated tenders, and exactly one fulfillmentUBM 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

When tenders are not allocated to fulfillments, a basket with one fulfillment has every tender allocated to it automatically, while a basket with more than one fulfillment is refused with 400 Bad Request, because UBM will not guess which fulfillment a payment is for.

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 a customerOrderId: the conflicting change was a checkout of this basket, typically your own retry or a double-submit, and that one created the order. Use that customerOrderId; do not check out again.
When the change landsWhat you get
Before checkout's first readCheckout 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 flightCheckout 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 existsCheckout 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.

After checkout the order can gain lines the basket never had. The basket keeps socks grey ×2 and a mug. When the store has only one grey pack and the picker substitutes a black one, the customer order has grey ×1 with 1 cancelled, a new line for black ×1, and the mug. Read the lines from the customer order.

⚠ 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:

  1. Add every item, coupon and loyalty membership first — these are the calls that reprice.
  2. Add the customer and the fulfillments. Neither reprices, so their order does not matter.
  3. Read the calculated result and take the order total from it.
  4. 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.
  5. 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.