BYO checkout API
BYO checkout means the tenant owns the checkout UI, but litecommerce owns the commerce state. Your storefront collects cart, customer, address, and payment UI inputs; the API computes the authoritative total, creates the Stripe PaymentIntent, receives the webhook, and confirms the order.
This is the same public contract hosted litecheckout uses under the hood. The hosted litecheckout app is a separate M3 surface, but a custom storefront does not need to wait for hosted pages to use the checkout-session API.
All paths below are under /api/v1.
What BYO can and cannot own
| Area | BYO storefront owns | litecommerce owns |
|---|---|---|
| UI | Cart, checkout form, Payment Element container, confirmation/status pages | None |
| Tenant context | x-organization-slug on standard public checkout calls | Resolving the organization and enforcing tenant scope |
| Pricing | Item refs, quantities, coupon code, customer/contact/address inputs, shipping-rate selection | Live repricing, discounts, shipping rates, tax snapshot, binding total |
| Payment | Rendering Stripe Payment Element with the returned publishableKey, optional stripeAccountId, and clientSecret | Creating/reusing Stripe objects through PaymentAttempt |
| Order creation | Status polling / confirmation UX | Webhook-authoritative payment state and order creation |
The storefront must never create Stripe PaymentIntents directly. It asks litecommerce for the payment handoff and renders the returned client secret.
Flow
1. Create a checkout session
POST /api/v1/public/checkout/sessions
Send x-organization-slug. The request carries catalog references and customer
draft information, not prices. The response returns a repriced summary plus the
raw session token exactly once.
const session = await api("/public/checkout/sessions", {
method: "POST",
headers: { "x-organization-slug": tenantSlug },
body: JSON.stringify({
currency: "USD",
customer: {
email: "dana@example.com",
name: "Dana Reyes",
},
lines: [
{ itemId: "item_...", variantId: "variant_...", quantity: 1 },
],
}),
});
session.token; // store client-side for this checkout only
session.totalInCents; // server-computed estimateThe session summary also includes customer-facing action URLs:
returnToStoreUrl, postCheckoutRedirectUrl, checkoutRetryUrl,
checkoutCancelUrl, accountUrl, and accountSignInUrl. These are resolved by
the API from tenant checkout settings plus trusted defaults; use them as-is for
confirmation, retry/cancel, return-to-store, and account CTAs, and render no
link when a field is null.
2. Reprice as the customer edits
POST /api/v1/public/checkout/sessions/:token/reprice
Call this when cart lines, coupon, customer, address, or shipping-rate inputs change. Reprice is mutable: it updates the persisted estimate but does not lock the total.
3. Offer shipping rates (when you've configured them)
GET /api/v1/public/shipping/zones
Once you've captured a delivery address, read the tenant's active shipping zones
— each with its rates inlined, ordered by sortOrder, name — and let the
customer pick one. Send x-organization-slug.
Resend the chosen rate's id as shippingRateId on reprice (to preview
the new total) and on bind (to charge it). litecommerce, not the storefront,
owns the shipping math: a reprice previews the charge and the bind folds it into
the binding total.
const zones = await api("/public/shipping/zones", {
headers: { "x-organization-slug": tenantSlug },
});
// Each zone inlines its `rates`; a rate's `id` is what you send back.
// Customer picks a rate; resend its id on the next reprice and on bind:
// body: { ...rest, shippingRateId: rate.id }Rate-required at bind. When the delivery address has applicable rates, the
bind must carry a shippingRateId — omitting it returns 400 (a reprice
still previews freely). When the destination has no applicable rates, omit it and
no shipping is charged. A selected rate requires a valid shipping/billing
address; a free-shipping discount keeps the method but zeroes the charge.
4. Bind before payment
POST /api/v1/public/checkout/sessions/:token/bind
Bind performs the final server-side reprice and marks the session as binding.
The effective customer name and email must be non-blank. If the destination has
applicable shipping rates, the bind must carry the selected shippingRateId
(see Offer shipping rates above) or it returns 400. When a tax provider is
enabled, the bind step uses the captured address to compute destination-based
tax — over products and the charged shipping — and stores the tax snapshot.
5. Request the payment handoff
POST /api/v1/public/checkout/sessions/:token/payment-session
This route is token-authenticated and does not require
x-organization-slug; the opaque session token resolves the tenant. The response
contains a Stripe clientSecret for the Payment Element, the browser-safe
publishableKey from the same API environment that created the PaymentIntent,
the optional connected stripeAccountId, and the internal payment attempt id.
const handoff = await api(
`/public/checkout/sessions/${session.token}/payment-session`,
{ method: "POST" },
);
const stripe = await loadStripe(
handoff.publishableKey,
handoff.stripeAccountId
? { stripeAccount: handoff.stripeAccountId }
: undefined,
);
// Render Stripe Payment Element with handoff.clientSecret.
// Do not create or mutate Stripe objects in the storefront.6. Confirm payment client-side, trust the webhook server-side
The browser confirms the Payment Element with Stripe. The customer can see an optimistic processing state, but the order is not final until litecommerce receives the Stripe webhook, marks the payment attempt, and creates the order. Read the checkout session or customer/order status surfaces for the durable result.
Access model
- Checkout session create/reprice/bind/read and the shipping-zones read use
x-organization-slugand no bearer token. - The payment-session route uses only the opaque checkout token.
- Customer account routes use
x-organization-slugplusCustomerSessionbearer auth. - Scoped customer action links use their token plus tenant context.
Adjacent checkout modes
Bookings now use the same CheckoutSession, PaymentAttempt, webhook, and Sales
rails after a booking hold or approval becomes payable, but their availability
and capacity contract is separate from an ordinary retail cart. Start with
Booking availability, holds & lifecycle; do not put a
booking into lines on the retail session create route.
Shared-commerce quote/invoice payment actions also have their own entry routes before they reach the payment rail. Subscriptions remain capability-gated; do not invent renewal, prepaid-term, or recurring-payment APIs until they appear in the OpenAPI reference.
Related
- Storefront integration — how checkout fits with catalog, pricing previews, shipping, and returns.
- Orders & fulfillment — legacy order creation and merchant order management.
- Booking availability, holds & lifecycle — the booking search, capacity claim, approval, and checkout boundary.
- API reference — exact request and response schemas.