Checkouts API

A checkout is the heavyweight, TTL'd transactional session. It freezes a cart's line items, walks the FSM (address → shipping → payment → processing → completed), and on success creates an order.

State machine

pending     created from cart, no address yet
  │
address     email + shipping_address set; billing optional
  │
shipping    method + rate (+ optional pickup_point) chosen
  │
payment     payment method picked, totals frozen
  │
processing  start_payment called, awaiting confirm
  │
  ├──▶ completed    order created, inventory committed, email queued
  └──▶ failed       payment declined; can retry → back to payment

abandoned   TTL expired (default 60min); reservations released

Phase progresses on each successful PATCH. Reverse jumps allowed only failed → payment for retry.

Create from cart

curl -X POST "$SF/checkouts.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{"cart_token": "8H3xa9...", "email": "[email protected]"}'

This atomically:

  1. Validates the cart is active and non-empty
  2. Snapshots all line items into checkout_line_items with frozen prices + tax rates
  3. Reserves inventory at the highest-priority active location (per commerce.checkout.reserve_at)
  4. Recomputes totals
  5. Sets expires_at = now() + commerce.checkout.session_ttl_minutes (default 60)

Returns the checkout with its own token.

Set address

curl -X PUT "$SF/checkouts/{token}/address.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "shipping_address": {
      "first_name": "Anna",
      "last_name": "Nováková",
      "address1": "Plzeňská 8",
      "city": "Praha",
      "country_code": "CZ",
      "zip": "15000",
      "phone": "+420..."
    },
    "billing_address": null,
    "same_as_shipping": true,
    "customer_note": "Prosím nezvonit, dítě spí."
  }'

Validates that both addresses (or just shipping if same_as_shipping=true) have address1 + city + country_code. Phase advances: pending → address.

Quote shipping

curl "$SF/checkouts/{token}/shipping_quote.json" \
  -H "X-Storefront-Key: $SK"

Computed against the checkout's destination + line weights + subtotal

  • currency. See Shipping for the engine.

Set shipping

curl -X PUT "$SF/checkouts/{token}/shipping.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{
    "shipping_method_id": 308...,
    "shipping_rate_id": 308...,
    "pickup_point_id": null
  }'

Validates currency matches, pickup-point requirement enforced, etc. Recomputes totals (shipping_cents + shipping VAT). Phase: address → shipping.

Apply discount code

curl -X POST "$SF/checkouts/{token}/discount.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{"code":"SAVE10"}'

See Discounts for the validation rules. Stack multiple stackable codes; non-stackable codes replace. Czech VAT math: tax computed on post-discount totals.

Remove with DELETE /checkouts/{token}/discount/{code}.json.

B2B VAT validation

curl -X PUT "$SF/checkouts/{token}/vat.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{"vat_country_code":"DE","vat_number":"123456789"}'

Triggers VIES validation (with cache) and applies reverse charge if eligible. See Tax.

Set payment method

curl -X PUT "$SF/checkouts/{token}/payment_method.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{"payment_method":"cod"}'

Method must match a registered connector (currently cod and bank_transfer; see Payments). Phase: shipping → payment.

Start payment

curl -X POST "$SF/checkouts/{token}/start_payment.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{
    "return_url": "https://shop.example.com/success",
    "cancel_url": "https://shop.example.com/cancel"
  }'

Calls the connector's start_payment. For card gateways, returns a redirect_url. For COD/bank, immediately ready to confirm. Creates a checkout_payment_attempts row. Phase: payment → processing.

Confirm

After the gateway redirects back (or for COD/bank, immediately):

curl -X POST "$SF/checkouts/{token}/confirm.json" \
  -H "X-Storefront-Key: $SK" -H "Content-Type: application/json" \
  -d '{"callback_payload": {"gateway_token":"..."}}'

Connector decides success or failure. On failure, phase moves to failed and the customer can retry with a different method. On success, the checkout is ready to complete (but not yet completed — the storefront calls complete separately).

Complete

curl -X POST "$SF/checkouts/{token}/complete.json" \
  -H "X-Storefront-Key: $SK" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'

Atomic in one DB transaction:

  1. Resolves customer (creates as guest if not seen before)
  2. Calls orders.create_order_from_checkout — full Order row with frozen line items, addresses, shipping snapshot, discount codes, totals
  3. Converts inventory reservations: reserved → committed
  4. Records discount_redemptions for usage tracking
  5. Marks the cart status=converted
  6. Sets checkout.phase = completed, order_id populated
  7. Publishes checkout.completed event
  8. Inserts an outbox_dispatches row for order.confirmation email

The Idempotency-Key header is required (Q9 design decision). Re-calling complete with the same key returns the same checkout — no duplicate order, no duplicate email.

Endpoints

POST   /storefront/2026-01/checkouts.json
GET    /storefront/2026-01/checkouts/{token}.json
PUT    /storefront/2026-01/checkouts/{token}/address.json
GET    /storefront/2026-01/checkouts/{token}/shipping_quote.json
PUT    /storefront/2026-01/checkouts/{token}/shipping.json
POST   /storefront/2026-01/checkouts/{token}/discount.json
DELETE /storefront/2026-01/checkouts/{token}/discount/{code}.json
PUT    /storefront/2026-01/checkouts/{token}/vat.json
PUT    /storefront/2026-01/checkouts/{token}/payment_method.json
POST   /storefront/2026-01/checkouts/{token}/start_payment.json
POST   /storefront/2026-01/checkouts/{token}/confirm.json
POST   /storefront/2026-01/checkouts/{token}/complete.json

Next: Payments.