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.
| Mechanism | Estimable today (display-only) | Binding at POST /public/orders | Bound by checkout sessions (M3) |
|---|---|---|---|
| Line items + subtotal | cart state | Yes — items[], subtotalInCents | — |
| Order total | subtotal (± your own estimates) | Yes — totalInCents == subtotalInCents | total incl. provider-gated tax, discounts, and selected shipping |
| Shipping | zone configuration from GET /public/shipping/zones; current options from checkout-session reprice | No — no shipping line persisted | Yes, for carts requiring shipping — bind locks the selected option's shippingInCents into the total |
| Coupons | POST /public/cart/apply-coupon (pure preview) | No — not attached | usage counted at placement |
| Auto-discounts | POST /public/auto-discounts/calculate | No — not attached | applied at checkout |
| Bundles | check-bundles / items/:slug/bundles | No — not attached | applied at checkout |
| Tax | no public estimate endpoint; cart display estimate only | No — taxInCents is 0 on legacy orders | provider-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
subtotalInCentsor label the extras "estimated, finalized at checkout." For M3 checkout sessions whose carts require shipping, select from reprice'savailableShippingOptionsand read the tax- and shipping-inclusive total afterbind;repricecan still returntaxInCents=0because 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:
-
Catalog —
/guides/catalog.GET /public/itemsand/public/items/:slugfor product + variant data (public reads returnACTIVEonly). -
Collections & pages —
/guides/collections-pages.GET /public/collections,/public/collections/:slug/items, and/public/pagesfor merchandising + CMS content. -
Inventory —
/guides/inventory.GET /public/inventory/:itemIdfor 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), andavailableQuantityis the units sellable now. For aSALEitem with variants, the line must name one exact livevariantId; an item-level target is refused, including when only archived variant history remains. Omission ornulltargets 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 itsavailableQuantity— 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/sessionschecks server-repricedSALEquantities before persisting a session, including expanded components of composed products. A measured shortfall returns HTTP 409 withcode: "RESOURCE_CONFLICT",details.reason: "INSUFFICIENT_INVENTORY", anddetails.lines[]naming every short stock target. No session is created on that refusal; correct the quantity in the storefront and submit create again.bindrechecks 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
SALEstock 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.RENTALandSERVICElines are outside this guard entirely — rental capacity comes from the booking/availability path, and services have no stock.POST /public/availability/checkremains the booking/rental date-range preflight (it requiresstartDate/endDate, matchesRENTALitems only, and reports everySALEline as unavailable), so do not wire it into a retail cart. -
Pricing —
/guides/pricing. The cart-math previews:POST /public/cart/apply-coupon,POST /public/auto-discounts/calculate,POST /public/cart/check-bundles, and the proactiveGET /public/items/:slug/bundles. All display-only (see the boundary above). -
Shipping —
/guides/shipping. UseGET /public/shipping/zonesonly for zone configuration and live-rate eligibility. For a cart with shippable lines on the M3 checkout-session path, create a session, callrepricewith the complete address, render its fullavailableShippingOptionsset, and send the selected option'srateIdasshippingRateIdon the next reprice and onbind. Bind locks that shipping charge before payment or confirmation. -
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 usex-organization-slug; thepayment-sessionhandoff 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. -
Legacy order path —
/guides/orders. If the storefront is not using checkout sessions,POST /public/orderscan 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. -
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.
Related
- Tenants & context — the
x-organization-slugheader - Orders & fulfillment — the full order contract + the legacy order persistence callout
- BYO checkout API — the checkout-session and payment handoff sequence for custom storefronts
- Error envelope — the shape every failed call returns