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.

POST …/baskets/{basketId}/checkout

├─ validate the basket and its calculated result
├─ validate tenders against active tender configurations
├─ allocate tenders to fulfillments
├─ validate each fulfillment's payment against its own share

├─▶ create the customer order (Customer Order service)
└─▶ mark the basket 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 found400 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 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 fulfillmentsSee below

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 cannot — there is no later opportunity to collect.

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🛑 Rejected. Delivery orders must be paid in full
Less than its share✅ Accepted — the store collects the balance🛑 Rejected
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 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 short, 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. 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. UBM's rule is a floor on what it will accept, not a statement about what your channel should require.

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.

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

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.

After checkout

The customer order is created first, then the basket is marked COMPLETED. From that point:

  • The basket is read-only and cannot be revived. Reads and deletes still work.
  • 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.

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