| owner | commerce-team |
|---|---|
| reviewers | payments-team, inventory-team |
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.
The customer presses "Place order" on the cart page.
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 |
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;
The reservation expires after 15 minutes[1] if the order is not confirmed.
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.
Stored with the order and used to capture the payment at shipping.
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;
}
Returns the order ID and the expected delivery date.
Shows the order number and sends a confirmation email.
The gateway returns a decline code. See Payment retry flow for the codes that are retried.
Releases the reservation at once, so that other customers can buy the items.
Warning
Do not show the decline code to the customer. Show "Your payment could not be completed." and let them choose another payment method.
The flow starts when the gateway declines an authorization that was started by a background job, such as a subscription renewal.
| Decline code | Retry |
|---|---|
processing_error |
Yes |
try_again_later |
Yes |
insufficient_funds |
No |
card_declined |
No |
Waits 1 minute, 5 minutes, then 30 minutes.
Uses the same idempotency key as the first attempt.
Checks the result of the retried authorization.
Updates the order status to paid.
Releases the stock and cancels the order.
The order continues to shipping.
Sends an email asking the customer to update their payment method.
The order is saved and waits for payment.
The payment was authorized.
The carrier has the parcel. The payment is captured at this point.
The carrier reported the delivery.
The order was cancelled before shipping. The authorization is voided.
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. |
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. |
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>| 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 |
Opens the top page. The cart is empty by now.
Opens the order history, with this order at the top.
Refunds can be requested within 30 days of delivery.
Refunds the captured amount to the original payment method.
Tells the customer that the refund can take up to 10 days to appear.
Returned items are checked and added back to the stock.
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.
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.