Skip to content
Elara One docs

Checkout and orders

Set up payment with Stripe or PayPal, place the order exactly once, give guests access to their order, and follow an order through its states.

Checkout turns a ready cart into an order. Your storefront sets up the payment with the store's own Stripe or PayPal account, the shopper pays in the provider's own form, and one call places the order. With an Idempotency-Key, that call can be retried safely and still places exactly one order.

Before you start

A cart is ready for checkout when it has at least one line, a billing address, and, if anything in it ships, a shipping address and a chosen shipping option. See carts. A cart that is not ready is refused with 422 VALIDATION_FAILED.

The steps

Read the checkout configuration

GET /v1/checkout/config describes the store's payment set-up. It needs no cart.

Response
{
  "data": {
    "gateways": ["stripe", "paypal"],
    "currency": "GBP",
    "country": "GB",
    "gateway_mode": "live",
    "stripe_publishable_key": "pk_live_…",
    "stripe_capture_method": "manual",
    "paypal_client_id": "AX…"
  }
}
FieldUse it to
gatewaysDecide which ways to pay to offer
gateway_modetest or live: show a "test payments" banner in test mode, and load PayPal's script for the same mode
stripe_publishable_keyInitialise Stripe.js
stripe_capture_methodmanual or automatic. Pass it as Elements' captureMethod if you mount the Payment Element before creating the PaymentIntent.
paypal_client_idThe client-id of PayPal's JavaScript SDK

Set up the payment

Stripe. POST /v1/checkout/payment-intent (with X-Cart-Token) creates a PaymentIntent for the cart's current total and returns its client_secret and the publishable_key. Mount Stripe's Payment Element with them, and let the shopper confirm the payment in the browser.

Each cart has one open PaymentIntent. Calling the endpoint again after the cart changes reuses it with the new total when that is possible, so call it again whenever the total changes before payment.

PayPal. POST /v1/checkout/paypal/order (with X-Cart-Token) creates a PayPal order and returns its paypal_order_id. Return it from the PayPal buttons' createOrder callback.

Place the order

For Stripe, once the shopper has confirmed the payment, call POST /v1/checkout/complete with the PaymentIntent's id. For PayPal, once the shopper approves, call POST /v1/checkout/paypal/capture with the PayPal order id. Both place the order and return 201 Created. Send an Idempotency-Key on both.

curl -X POST https://api.elaraone.app/api/v1/checkout/complete \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Cart-Token: $CART_TOKEN" \
  -H "Idempotency-Key: 9b2c6f0e-4a7d-4c1e-9f3b-2d8a5e7c1b04" \
  -H "Content-Type: application/json" \
  -d '{ "payment": { "gateway": "stripe", "payment_intent": "pi_3QxYz…" } }'

For PayPal, the body of POST /v1/checkout/paypal/capture is { "paypal_order_id": "5O190127TN364715T" }. POST /v1/checkout/complete also accepts { "payment": { "gateway": "paypal", "paypal_order_id": "…" } }.

The order

201 Created (trimmed)
{
  "data": {
    "id": 3051,
    "reference": "00003051",
    "status": "payment-received",
    "status_label": "Payment Received",
    "placed_at": "2026-10-24T10:12:31+00:00",
    "dispatched_at": null,
    "delivered_at": null,
    "cancelled_at": null,
    "lines": [
      {
        "id": 9120,
        "type": "physical",
        "description": "Linen cushion",
        "identifier": "CUSH-LIN-NAT",
        "option": null,
        "quantity": 2,
        "unit_price": { "amount": 3400, "currency": "GBP", "formatted": "£34.00" },
        "total": { "amount": 6800, "currency": "GBP", "formatted": "£68.00" }
      }
    ],
    "shipping_method": { "identifier": "standard", "name": "Standard delivery", "kind": "delivery", "collection_location": null },
    "shipments": [],
    "sub_total": { "amount": 6800, "currency": "GBP", "formatted": "£68.00" },
    "shipping_total": { "amount": 495, "currency": "GBP", "formatted": "£4.95" },
    "tax_total": { "amount": 1216, "currency": "GBP", "formatted": "£12.16" },
    "total": { "amount": 7295, "currency": "GBP", "formatted": "£72.95" }
  },
  "meta": {
    "order_access_token": "eyJ…",
    "order_access_token_expires_at": "2026-10-24T11:12:31+00:00"
  }
}

After a successful checkout the cart token stops working (404 CART_NOT_FOUND). Create a new cart for the shopper's next order. The store's order confirmation email is sent automatically, if the store has it switched on.

Exactly once

Send an Idempotency-Key on POST /v1/checkout/complete and POST /v1/checkout/paypal/capture. If the response is lost (a timeout, a dropped connection, a double click), retry with the same key and the same body:

  • if the first request placed the order, you get the same 201 back, with Idempotent-Replayed: true, and no second order is created;
  • if the first request is still running, you get 409 IDEMPOTENCY_IN_PROGRESS; wait a moment and retry with the same key;
  • if the first request failed, the retry runs again, so a shopper who fixes their card can pay with the same key.

A PayPal capture is recorded before the order is placed, so a retry never captures the payment twice. See idempotency keys.

What can go wrong

Statuserror.codeWhat happenedWhat to do
402PAYMENT_FAILEDThe payment was declined or could not be taken. The cart and stock are unchanged.Show the reason from details.decline_code and let the shopper try again. See payments.
409SHIPPING_OPTION_CHANGEDThe chosen shipping option is no longer offered, or its price changed since it was chosen. Nothing was charged.Show details.options (the options on offer now), let the shopper choose again with PUT /v1/cart/shipping-option, then set up the payment again.
409PROMOTIONS_CHANGEDThe basket's discounts changed since the payment was set up (an offer ended, ran out of uses, or a first-order offer no longer applies). Nothing was charged.Show the new total from details.cart and set up the payment again. details.reason is changed, used_up, limit_reached or first_order.
409PAYMENT_REFUNDEDThe payment was taken but the order could no longer be placed (stock gone, cart changed), so the payment was refunded.Tell the shopper, and start a new checkout.
409PAYMENT_REFUND_PENDINGAs above, but the automatic refund is still being retried. The store has been told.Tell the shopper their payment will be refunded. Do not ask them to pay again for this cart.
409PAYMENT_ALREADY_USEDThis payment already paid for another checkout.Set up a new payment.
409PAYMENT_SETUP_IN_PROGRESSAnother request is setting up this cart's PaymentIntent.Retry shortly; you get the same intent.
422VALIDATION_FAILEDThe cart is not ready, or an item no longer has enough stock.Show details and send the shopper back to fix the cart.
422PAYMENT_GATEWAY_UNAVAILABLEThe gateway is unknown or not set up for this store.Only offer gateways listed in checkout/config.

SHIPPING_OPTION_CHANGED can also come back from POST /v1/checkout/payment-intent and POST /v1/checkout/paypal/order: the engine checks the shipping price before any payment is set up.

Order states

statusLabelMoves to
awaiting-paymentAwaiting Paymentpayment-offline, payment-received, cancelled
payment-offlinePayment Offlinepayment-received, cancelled
payment-receivedPayment Receiveddispatched, cancelled
dispatchedDispatcheddelivered
deliveredDeliveredNothing (final)
cancelledCancelledNothing (final)

Branch on status, which never changes meaning, and show status_label, which is the store's own wording. dispatched_at, delivered_at and cancelled_at are filled in when the order reaches that state, and shipments lists the parcels with their carrier and tracking details once the order is dispatched. A cancelled order's stock is put back.

Guest orders

A guest has no account, so a guest order comes with a short-lived access token in meta.order_access_token, and meta.order_access_token_expires_at says when it stops working. Use it straight away to show the confirmation page:

curl https://api.elaraone.app/api/v1/orders/guest/00003051 \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Order-Access-Token: $ORDER_ACCESS_TOKEN"

To let a guest look up an order later, ask for the order reference and the email used at checkout, and call POST /v1/orders/lookup:

POST /v1/orders/lookup
{ "reference": "00003051", "email": "[email protected]" }

It returns the order and a fresh meta.order_access_token. A reference and email that do not match return 404. Lookups share the sign-in rate limit (10 a minute per store and IP), because a reference and email together act like a password.

A missing, tampered or expired access token returns 403 ORDER_ACCESS_TOKEN_INVALID. details.reason is expired (ask for the email again) or invalid.

Signed-in customers see their orders with GET /v1/orders and GET /v1/orders/{reference}. See customers.