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 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 estimate2. 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 plus the internal
payment attempt id.
const handoff = await api(
`/public/checkout/sessions/${session.token}/payment-session`,
{ method: "POST" },
);
// 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.
Planned modes
Shared-commerce documents, bookings, and subscriptions will use this same spine later, but their checkout modes are not part of the current BYO checkout contract. Do not invent quote/invoice/contract actions, booking holds, subscription renewals, prepaid terms, or recurring payment APIs until those M3.5/M4/M5 contracts ship and 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.
- API reference — exact request and response schemas.