Checkout Design
Version
2.1
Product
Example
Status
In review
Updated
2026-10-01
Other metadata (2)
ownercommerce-team
reviewerspayments-team, inventory-team

Checkout Design

Note

This document describes how an order is placed from the cart page, from the "Place order" button to the order confirmation. Shipping and invoicing are described in separate documents.

Checkout sequence

Payment GatewayInventoryOrder APIWeb AppPayment GatewayInventoryOrder APIWeb Appalt[Payment authorized][Payment declined]CustomerPlace order1POST /orders2Reserve stock3Reservation ID4Authorize payment5Authorization code6Save order as confirmed7201 Created8Show order confirmation9Declined10Release stock11402 Payment Required12Customer

1. Place order

The customer presses "Place order" on the cart page.

  • The button is disabled until the shipping address and payment method are chosen.
  • The cart is sent with an idempotency key, so pressing the button twice creates only one order.

2. POST /orders

POST /orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-0b7d-4f3a-9c55-1d2e8a7b4c10
{
  "cartId": "c_8241",
  "shipping": { "addressId": "a_17", "timeSlot": "am" },
  "payment": { "methodId": "pm_card_visa" },
  "items": [{ "sku": "SKU-1042", "quantity": 2 }]
}
Status When
201 Order confirmed
402 Payment declined
409 An item is out of stock

3. Reserve stock

Reserves every item of the cart in one transaction. If one item is short, nothing is reserved and the API returns 409 Conflict.

UPDATE stock
SET reserved = reserved + $2
WHERE sku = $1 AND quantity - reserved >= $2;

4Reservation ID

The reservation expires after 15 minutes[1] if the order is not confirmed.

5. Authorize payment

Authorizes the order total. The amount is captured later, when the order is shipped.

Important

Never send the card number to the Order API. The Web App sends it to the Payment Gateway directly and passes only the payment.methodId it gets back.

6Authorization code

Stored with the order and used to capture the payment at shipping.

7Confirm order

Saves the order with the status confirmed and links the reservation to it.

export async function confirmOrder(tx: Tx, order: NewOrder, auth: Authorization): Promise<Order> {
  const saved = await tx.orders.insert({ ...order, status: 'confirmed', authCode: auth.code });
  await tx.reservations.attach(order.reservationId, saved.id);
  return saved;
}

8201 Created

Returns the order ID and the expected delivery date.

9. Show order confirmation

Shows the order number and sends a confirmation email.

  • Order number and items
  • Expected delivery date
  • Link to cancel within 30 minutes

10Declined

The gateway returns a decline code. See Payment retry flow for the codes that are retried.

11Release stock

Releases the reservation at once, so that other customers can buy the items.

12402 Payment Required

Warning

Do not show the decline code to the customer. Show "Your payment could not be completed." and let them choose another payment method.

Payment retry flow

No

Yes

Yes

No, under 3 tries

No, 3 tries

Payment declined

Retryable error?

Cancel order

Wait with backoff

Retry authorization

Authorized?

Mark order as paid

Done

Notify customer

Payment declined

The flow starts when the gateway declines an authorization that was started by a background job, such as a subscription renewal.

Retryable error?

Decline code Retry
processing_error Yes
try_again_later Yes
insufficient_funds No
card_declined No

Wait with backoff

Waits 1 minute, 5 minutes, then 30 minutes.

Retry authorization

Uses the same idempotency key as the first attempt.

Authorized?

Checks the result of the retried authorization.

Complete payment

Updates the order status to paid.

Cancel order

Releases the stock and cancels the order.

Done

The order continues to shipping.

Notify customer

Sends an email asking the customer to update their payment method.

Order status

Created

Paid

Shipped

Delivered

Cancelled

Created

The order is saved and waits for payment.

Paid

The payment was authorized.

Shipped

The carrier has the parcel. The payment is captured at this point.

Delivered

The carrier reported the delivery.

Cancelled

The order was cancelled before shipping. The authorization is voided.

Error codes

The errors of the Order API. The body of an error response is { "code": "ORD-011", "message": "..." }.

Code HTTP status Meaning Client action
ORD-001 400 The request body is not valid JSON. Fix the request.
ORD-002 400 A required field is missing. Fix the request.
ORD-003 400 The Idempotency-Key header is missing. Add the header.
ORD-004 400 quantity is not a positive integer. Fix the request.
ORD-005 400 Too many items in one order (max 100). Split the order.
ORD-006 401 The access token is missing or has expired. Log in again.
ORD-007 402 The payment was declined. Ask for another payment method.
ORD-008 402 The card has expired. Ask for another card.
ORD-009 402 The 3-D Secure check failed. Retry the check.
ORD-010 403 The account is suspended. Show the support contact.
ORD-011 404 The cart does not exist. Reload the cart.
ORD-012 404 The shipping address does not exist. Ask for an address again.
ORD-013 404 The payment method does not exist. Ask for a payment method again.
ORD-014 409 An item is out of stock. Show the stock and ask again.
ORD-015 409 The cart changed while checking out. Show the new cart.
ORD-016 409 The Idempotency-Key was used for another cart. Create a new key.
ORD-017 422 The address is outside the delivery area. Ask for another address.
ORD-018 422 The total is below the minimum order. Show the minimum.
ORD-019 429 Too many requests. Retry after Retry-After.
ORD-020 502 The payment gateway did not answer. Check the order list before retrying.
ORD-021 503 The inventory service is unavailable. Retry later.

Request body of POST /orders

The fields of the JSON body. "Required" of a nested field applies when its parent is sent.

No Level FieldPhysical name Physical name Type Required Description
1 1 Cart IDcartId cartId string Yes The cart to check out. It must belong to the signed-in customer and must not have been checked out already.
2 1 Shippingshipping shipping object Yes
3 2 Address IDaddressId addressId string Yes One of the customer's saved addresses. An address outside the delivery area is rejected, so the order screen grays out the addresses that cannot be delivered to.
4 2 Delivery datedeliveryDate deliveryDate string YYYY-MM-DD. The earliest date when omitted. A fully booked date or a date more than 14 days ahead is rejected.
5 2 Time slottimeSlot timeSlot string am, pm or evening. Any time of the day when omitted. evening is not available on Sundays.
6 1 Paymentpayment payment object Yes
7 2 Payment method IDmethodId methodId string Yes A saved card or wallet. A card that has expired or failed the last authorization cannot be used until the customer updates it.
8 2 Coupon codecouponCode couponCode string Only one coupon per order. An expired coupon is ignored, and the response tells why.
9 1 Itemsitems items array Yes 1 to 100 items. Each SKU appears only once; send the total in quantity.
10 2 SKUsku sku string Yes
11 2 Quantityquantity quantity integer Yes 1 to 99. Also limited by the stock and by the per-customer limit of the product, whichever is smaller.
12 2 Gift wrapgiftWrap giftWrap boolean false when omitted. Wrapping is charged per item and is not available for some products such as furniture.

Order confirmation screen

The screen shown by step 9 of the checkout. The numbers of the wireframe are those of the items below.

<html lang="en"><head>

<title>Order confirmation</title>
<style>/* Shared styles of the wireframes: a plain, gray look, so that they read as mock-ups rather than finished screens. */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", Meiryo, "Noto Sans JP", sans-serif;
  font-size: 14px;
  color: #333;
  background: #fafafa;
}
header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  background: #e0e0e0;
  font-weight: bold;
}
header img { width: 24px; height: 24px; }
main { padding: 20px 16px; }
h1 { margin: 0 0 16px; font-size: 20px; }
label { display: block; margin-bottom: 12px; }
label span { display: block; margin-bottom: 4px; color: #666; font-size: 12px; }
input[type="text"],
input[type="password"] {
  box-sizing: border-box;
  width: 100%;
  padding: 8px;
  border: 1px solid #999;
  border-radius: 4px;
  background: #fff;
}
.check { display: flex; align-items: center; gap: 6px; }
.check span { display: inline; margin: 0; font-size: 14px; color: #333; }
button {
  width: 100%;
  padding: 10px;
  border: none;
  border-radius: 4px;
  background: #666;
  color: #fff;
  font-size: 15px;
}
.error {
  margin-bottom: 12px;
  padding: 8px;
  border: 1px dashed #c00;
  color: #c00;
  font-size: 12px;
}
.links { margin-top: 16px; text-align: center; }
a { color: #333; }
.done { margin-bottom: 16px; padding: 12px; border-radius: 4px; background: #e8e8e8; text-align: center; }
.done strong { display: block; margin-top: 4px; font-size: 18px; letter-spacing: 1px; }
dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; margin: 0 0 16px; }
dt { color: #666; }
dd { margin: 0; text-align: right; }
.items { margin: 0 0 12px; padding: 0; list-style: none; border-top: 1px solid #ccc; }
.items li { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ccc; }
.total { font-weight: bold; }
</style>
</head>
<body>
<header><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjODg4Ii8+PHBhdGggZD0iTTcgMTJsMyAzIDctNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuNSIvPjwvc3ZnPgo=" alt=""> Example Shop</header>
<main>
  <div class="done">Thank you for your order. <strong data-item="1">ORD-2026-000123</strong></div>
  <ul class="items">
    <li data-item="2"><span>Desk lamp × 1</span><span>$39.00</span></li>
    <li data-item="2"><span>LED bulb × 2</span><span>$12.00</span></li>
  </ul>
  <dl>
    <dt>Total</dt><dd class="total" data-item="3">$51.00</dd>
    <dt>Delivery</dt><dd data-item="4">Oct 28, morning</dd>
  </dl>
  <button data-item="5">Continue shopping</button>
  <p class="links"><a href="#" data-item="6">View your orders</a></p>
</main>


</body></html>
1
2
3
4
5
6
No ItemName Name Type Description
1 Order numberorderNo orderNo Text From the response of POST /orders.
2 Itemsitems items List One row per SKU, with its quantity and subtotal.
3 Totaltotal total Text Tax and shipping included.
4 Deliverydelivery delivery Text The date and time slot chosen.
5 Continue shoppingcontinue continue Button
6 View your ordersorders orders Link

5. Continue shopping

Opens the top page. The cart is empty by now.

6. View your orders

Opens the order history, with this order at the top.

Refund (draft)

Payment GatewayOrder APIPayment GatewayOrder APICustomerRequest refund1Refund payment2Refund ID3Send refund email4Customer

1Request refund

Refunds can be requested within 30 days of delivery.

2Refund payment

Refunds the captured amount to the original payment method.

4Send refund email

Tells the customer that the refund can take up to 10 days to appear.

Restock items

Returned items are checked and added back to the stock.

Open items

Tip

To try the checkout locally, start the gateway mock with docker compose up -d payment-mock and use the card 4242 4242 4242 4242.

Why reserve stock before authorizing the payment?

If the payment is authorized first and the stock then runs out, the authorization has to be voided, and some banks show it to the customer for several days.

Caution

Never log the authorization code or any part of the card number.


  1. Long enough for a 3-D Secure check, which can take several minutes. A job releases the expired reservations every minute. ↩︎

  2. The gateway keeps a key for 24 hours. A retry after that would create a second payment. ↩︎