Skip to content

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

AreaBYO storefront ownslitecommerce owns
UICart, checkout form, Payment Element container, confirmation/status pagesNone
Tenant contextx-organization-slug on standard public checkout callsResolving the organization and enforcing tenant scope
PricingItem refs, quantities, coupon code, customer/contact/address inputs, shipping-rate selectionLive repricing, discounts, shipping rates, tax snapshot, binding total
PaymentRendering Stripe Payment Element with the returned clientSecretCreating/reusing Stripe objects through PaymentAttempt
Order creationStatus polling / confirmation UXWebhook-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 estimate

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 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-slug and no bearer token.
  • The payment-session route uses only the opaque checkout token.
  • Customer account routes use x-organization-slug plus CustomerSession bearer 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.