Skip to content

Storefront integration

Most of these guides are written merchant-first (admin writes, role-gated). A storefront is the opposite: it's mostly public reads plus public checkout writes. Legacy storefronts place the order with POST /public/orders; M3 storefronts adopt checkout-session writes (create, reprice, bind) before payment/confirmation. This page sequences those paths and — more importantly — draws the one boundary that trips up every storefront: what litecommerce can estimate today vs. what it actually persists on an order.

Tenant-resolving public calls carry the tenant header (x-organization-slug) and no bearer token — public reads are not merchant or customer authenticated, but they still need tenant context or their documented token-only route. Token-only checkout handoff routes document their own exception. Paths are under /api/v1.

External storefront backends can alternatively fetch catalog data server-to-server from /api/v1/storefront/* with a tenant API key. Keep the key on the server only; do not send it from browser JavaScript. Those reads reuse the same public-safe item and collection projections as /api/v1/public/*, but the tenant is bound by the key rather than x-organization-slug.

The cart/checkout boundary (read this first)

Before a storefront adopts the M3 checkout-session flow, its cart total is only an estimate. The legacy POST /public/orders path persists line items only — its subtotalInCents is the sum of the lines, taxInCents is 0, and totalInCents equals the subtotal. Everything else — tax, shipping, coupons, auto-discounts, bundles — is a display-only estimate until the server-side checkout session is repriced and bound.

MechanismEstimable today (display-only)Binding at POST /public/ordersBound by checkout sessions (M3)
Line items + subtotalcart stateYes — items[], subtotalInCents—
Order totalsubtotal (± your own estimates)Yes — totalInCents == subtotalInCentstotal incl. provider-gated tax, discounts, and selected shipping
Shippingzone configuration from GET /public/shipping/zones; current options from checkout-session repriceNo — no shipping line persistedYes, for carts requiring shipping — bind locks the selected option's shippingInCents into the total
CouponsPOST /public/cart/apply-coupon (pure preview)No — not attachedusage counted at placement
Auto-discountsPOST /public/auto-discounts/calculateNo — not attachedapplied at checkout
Bundlescheck-bundles / items/:slug/bundlesNo — not attachedapplied at checkout
Taxno public estimate endpoint; cart display estimate onlyNo — taxInCents is 0 on legacy ordersprovider-gated tax calculation after bind

The rule: a storefront should not treat cart-side tax, shipping, or discount previews as binding. For the legacy public order path, reconcile the final display to subtotalInCents or label the extras "estimated, finalized at checkout." For M3 checkout sessions whose carts require shipping, select from reprice's availableShippingOptions and read the tax- and shipping-inclusive total after bind; reprice can still return taxInCents=0 because Stripe Tax is only computed when the session is bound with the captured address.

Stripe Tax is wired into the checkout-session bind flow (#449), but it is provider-gated and off by default. When a tax provider is enabled, bind computes destination-based tax from the captured address, persists an internal tax snapshot, and returns public taxInCents / totalInCents values. Until a storefront uses that flow, do not present client-side tax as authoritative.

The read/write sequence

A typical storefront wires these in roughly this order:

  1. Catalog — /guides/catalog. GET /public/items and /public/items/:slug for product + variant data (public reads return ACTIVE only).

  2. Collections & pages — /guides/collections-pages. GET /public/collections, /public/collections/:slug/items, and /public/pages for merchandising + CMS content.

  3. Inventory — /guides/inventory. GET /public/inventory/:itemId for stock state at the PDP and cart; it returns the item's publicly visible rows (non-public items and archived variants are filtered out; capped at 5000 rows with no truncation signal, #3090), and availableQuantity is the units sellable now. For a SALE item with variants, the line must name one exact live variantId; an item-level target is refused, including when only archived variant history remains. Omission or null targets the item-level row only for a genuinely variant-less item. Pick the row matching that valid line target and compare the requested quantity with its availableQuantity — that is the UX guard, not a guarantee: the read cannot reserve the unit.

    Session create is the per-cart stock check: POST /public/checkout/sessions checks server-repriced SALE quantities before persisting a session, including expanded components of composed products. A measured shortfall returns HTTP 409 with code: "RESOURCE_CONFLICT", details.reason: "INSUFFICIENT_INVENTORY", and details.lines[] naming every short stock target. No session is created on that refusal; correct the quantity in the storefront and submit create again. bind rechecks stock and returns the same shortfall shape while leaving the session recoverable. These reads reserve nothing; successful create or bind does not guarantee stock at confirmation. Counts describe exact item/variant units, or expanded component units for a composed selection, not package quantities.

    Checkout confirmation remains the final authority: it reserves inventory and rejects an oversell when the valid SALE stock target's exact (itemId, variantId) row exists. A missing exact row is not zero and passes the create and bind stock checks. If still missing at confirmation, the valid line is preserved as unbounded with no stock check (an item-level row does not cover a variant line), so an "availability unknown" state is the only guard that line gets. RENTAL and SERVICE lines are outside this guard entirely — rental capacity comes from the booking/availability path, and services have no stock. POST /public/availability/check remains the booking/rental date-range preflight (it requires startDate/endDate, matches RENTAL items only, and reports every SALE line as unavailable), so do not wire it into a retail cart.

  4. Pricing — /guides/pricing. The cart-math previews: POST /public/cart/apply-coupon, POST /public/auto-discounts/calculate, POST /public/cart/check-bundles, and the proactive GET /public/items/:slug/bundles. All display-only (see the boundary above).

  5. Shipping — /guides/shipping. Use GET /public/shipping/zones only for zone configuration and live-rate eligibility. For a cart with shippable lines on the M3 checkout-session path, create a session, call reprice with the complete address, render its full availableShippingOptions set, and send the selected option's rateId as shippingRateId on the next reprice and on bind. Bind locks that shipping charge before payment or confirmation.

  6. BYO checkout — /guides/byo-checkout. Use checkout sessions for the server-authoritative total and the litecommerce Stripe Payment Element handoff. Create/read/reprice/bind calls use x-organization-slug; the payment-session handoff is token-only and does not send that header. The storefront renders checkout UI; the API owns pricing, tax, payment attempts, webhook state, and order creation.

  7. Legacy order path — /guides/orders. If the storefront is not using checkout sessions, POST /public/orders can still place a line-item-only order; otherwise, use public order reads (/public/orders/:id, /number/:num) only to render confirmation + status after the checkout-session/payment flow creates the order.

  8. Returns — /guides/returns. The post-purchase flow, authenticated by matching the order's email.

The two reference storefronts (EKGIS Naturals + Idaho Pro Gear) consume exactly this surface — they're the working proof of the BYO-frontend model.