Build a custom storefront
Run your own storefront UI on Kentro's storefront infrastructure: buyer sign-in, catalog, cart, quote, checkout, orders and account.
A custom storefront replaces the rendering of a Kentro storefront with your own web application, while the catalog, prices, carts, checkout, orders and buyer accounts stay in Kentro. Your application's server calls the Store API for the buyer in front of it, the same way Kentro's own storefront templates do.
Who this is for
- Teams that need a design, a framework or a buying flow beyond the templates Kentro OS offers under
Storefronts. - Agencies building B2B ordering sites for Kentro tenants.
How it fits
The browser talks only to your server. Your server holds the API key and, for each browser session, the buyer's tokens or a guest token, and calls the Store API and Kentro's authentication service. Payment happens with the store's payment provider: checkout answers a hosted page URL or a client secret, and your server confirms the result when the buyer returns.
Browser Your storefront server Kentro
(pages, your cookie) (API key; buyer or guest
tokens per session)
sign in ----------> POST /auth/login/customer ------------> authentication
browse ----------> POST /catalog.list ------------> Store API
add to cart ---------> POST /cart.add (expectedRevision) -----> Store API
check out ----------> POST /checkout.start ------------> Store API
<---------- redirectUrl or clientSecret
pays with the provider, returns
----------> POST /checkout.confirm ------------> order placed
Server side only
The browser never calls the Store API and never sees the API key, a buyer token or a guest token. The Store API answers with no CORS headers for tenant origins.
Prerequisites
- A published storefront in Kentro OS (
Storefronts, thenPublish), on its managed address or a verified custom domain. A storefront that is not published answers404 NOT_FOUNDfor its host. - An API key whose permission group holds
storefront:serve.X-API-Keycarries the key's secret. - The storefront's host, sent as
X-Storefront-Hoston every call.
Objects involved
| Object | Operations |
|---|---|
| Store | store.get: branding, access rules, capabilities, the sign-up policy. |
| Session | session.get, session.guest, session.signup, account.confirm. |
| Catalog | catalog.categories, catalog.list, catalog.get, catalog.suggest, priced at the buyer's price list. |
| Cart | cart.get, cart.add, cart.addMany, cart.set, cart.remove, cart.update, cart.contact, with revision and ownershipVersion on every answer. |
| Quote | cart.get { "quote": true }: shipping, tax, credits, the amount to charge, and quoteFingerprint. |
| Checkout attempt | checkout.start, checkout.status, checkout.confirm, checkout.cancel. |
| Orders and invoices | orders.list, orders.get, orders.reorder; invoices.list, invoices.get, invoices.pay, invoices.payLink. |
| Account books | account.*, addresses.*, payments.* and carriers.*, each written with expectedVersion. |
| Subscriptions and applications | subscription.* for recurring orders; application.* for buyer applications. |
Sequence
- Read the store.
store.get(store-get) on every page request:access,publicPrices,guestPrices,guestCheckout,paymentProvider,fulfillment,currencyandacquisitiondecide what to render. - Identify the visitor. A guest gets a token from
session.guest(session-guest), only whenguestCheckoutis true. A buyer signs in withPOST https://apis.kentro.live/auth/login/customerand{ email, password, host }. A new buyer signs up withsession.signup(session-signup) and confirms withaccount.confirm(account-confirm).session.get(session-get) says what the tokens the server holds amount to. - Browse.
catalog.categories(catalog-categories) answers the navigation tree;catalog.listpages the products the buyer may see;catalog.get(catalog-get) answers one product with its variants;catalog.suggest(catalog-suggest) is the typeahead, limited to 30 calls per 3 seconds per identity. - Build the cart.
cart.get(cart-get), thencart.add(cart-add),cart.addMany(up to 100 lines),cart.set(cart-set) andcart.remove(cart-remove).cart.update(cart-update) sets fulfillment, addresses, payment method, shipping rate, coupon and notes; a guest sends email and addresses withcart.contact(cart-contact). Every mutation sendsexpectedRevisionfrom the last answer and answers the whole cart: render it, never recompute totals. - Quote.
cart.get { "quote": true }prices the cart and answerscheckoutAllowed,issues,shippingRates,charge,balanceDueandquoteFingerprint. A quote lives at most 10 minutes. - Start checkout.
checkout.start(checkout-start) withexpectedRevision, anidempotencyKeyand thequoteFingerprint. The answer'sstatesays what happens next:pending_paymentorneeds_actionwith aredirectUrlorclientSecret,completewith anorderId, ordeclinedwith the message to show. - Finish checkout. When the buyer returns from the provider,
checkout.confirm(checkout-confirm) verifies the payment and completes the order.checkout.status(checkout-status) polls an attempt;checkout.cancel(checkout-cancel) abandons one the provider confirms cannot complete. - After the order. Orders, reorders, invoices and the account:
orders.reorder(orders-reorder),invoices.pay(invoices-pay),account.get(account-get),addresses.save(addresses-save) andpayments.save(payments-save). The bodies of every step are in A buyer's journey. - Measure, optionally.
pixel.get(pixel-get) answers the storefront's Kentro Pixel public key for the browser SDK; see Measure with Kentro Pixel.
Edge cases
| Case | What the Store API answers | What to do |
|---|---|---|
| A guest with a cart signs in | 409 CART_CHOICE_REQUIRED on the next cart mutation | Send both tokens, read cart.choices, let the buyer pick, call cart.choose. |
| A stale cart in a second tab | 409 CONFLICT "Your cart changed. Review the current cart." | Read cart.get, show the current cart, let the buyer act again. |
| An unapproved or unconfirmed buyer | 403 APPROVAL_REQUIRED or 403 CONFIRMATION_REQUIRED | Show the pending-approval panel, or the confirmation step. |
| Prices hidden from this visitor | price is null; the cart answers pricesVisible: false | Render "sign in for prices" instead of zero. |
| An expired buyer token | 401 UNAUTHORIZED "Storefront authorization failed." | Refresh once with POST /auth/refresh, then retry. |
A lost checkout.start answer | checkout.status { idempotencyKey, quoteFingerprint } finds the attempt; never start a second checkout (409 CHECKOUT_PENDING). | |
| A declined payment | 200 with state: "declined" and a message | Show the message and return to the cart. |
| A product with variants added without one | 409 PRODUCT_VARIANT_REQUIRED | Send variantId. |
| A browser whose session changed | 409 CONFLICT "Your store or account changed. Reload before continuing." with X-Expected-Buyer | Reload the session and start over. |
| A degraded store answer | Response header X-Store-Degraded: payment-config | Do not cache that answer. |
Checklist
- Every Store API call comes from the server; the browser holds only your session cookie.
X-API-Key,X-Storefront-Host, and the buyer or guest token travel on every call;X-Expected-BuyerandX-Storefront-Client-Iptoo.- Tokens refresh before expiry or on the first
401. - Every cart mutation sends
expectedRevision, and the UI renders the cart the API answers. - Checkout sends an
idempotencyKeyand the currentquoteFingerprint, and recovers throughcheckout.status. - Amounts are rendered from integers in minor units with the store's
currency. - Nothing buyer-specific is cached: every answer carries
Cache-Control: no-store. - The Go-live checklist passes.
Build it with an AI agent
The prompt below starts a coding agent on this storefront. Agent setup and the other prompts: Build with AI agents.
Build it with an AI agent
Build a storefront whose server calls the Kentro Store API for catalog, buyer sign-in, cart, quote and checkout, in Next.js or any framework. Copy the prompt into Claude Code, Codex, Cursor or another coding agent. The prompt library explains how to run it.
You are building a custom storefront on the Kentro Store API. The user's own web application renders the pages, and its server calls the Store API for the buyer in front of it. Kentro keeps the catalog, prices, stock, buyer accounts, carts, quotes, checkout, orders and invoices. Use the framework the user names; default to Next.js with the App Router.
Read first. Fetch https://docs.kentro.io/llms.txt and these pages, and look up every operation and schema in them before you use it:
- https://docs.kentro.io/docs/ai-agent-instructions.md (rules you must follow)
- https://docs.kentro.io/reference/store-api-getting-started.md
- https://docs.kentro.io/reference/store-api-authentication.md
- https://docs.kentro.io/reference/store-api-conventions.md
- https://docs.kentro.io/reference/store-api-flows.md
- https://docs.kentro.io/docs/guide-custom-storefront.md
Ask the user for these before writing code:
1. The framework and where the application will run.
2. The host of a published storefront in the sandbox (Kentro OS at https://kentro.cc, `Storefronts`). It is the X-Storefront-Host value.
3. A sandbox API key whose permission group holds `storefront:serve`, stored server-side as KENTRO_API_KEY. Never in the chat, never in client code.
4. Which pages to build first: catalog, product, cart, checkout, account or orders.
Rules:
- Call the Store API only from the server: Server Components, Route Handlers or Server Actions in Next.js, server routes in other frameworks. No Client Component, browser script or NEXT_PUBLIC_ variable ever holds the key or a token. The API sends no CORS headers for browsers.
- Base URL https://apis.kentro.dev/store/v1 while building (production https://apis.kentro.live/store/v1); buyer sign-in at https://apis.kentro.dev/auth. Every operation is POST `/<family>.<operation>` with a JSON body, `{}` when it takes no fields.
- Every call sends X-API-Key and X-Storefront-Host (from configuration, lower case, no scheme). Buyer calls add X-Storefront-Buyer and guest calls add X-Storefront-Guest. Also send X-Storefront-Client-Ip and X-Expected-Buyer. The API does not read Authorization.
- Keep buyer and guest tokens on the server, keyed by an httpOnly session cookie. Refresh on 401 with POST /auth/refresh.
- Money is integers in minor units; format it with the store's currency.
- Every cart mutation sends expectedRevision and ownershipVersion from the last cart answer. On 409 CONFLICT, read cart.get again and re-render.
- checkout.start sends idempotencyKey, quoteFingerprint and expectedRevision. Never start a second checkout while one is pending.
- Never cache Store API answers across requests. Call only the operations the Store API reference documents. There is no Kentro SDK: write a small server-only client module.
Steps:
1. A server-only Kentro client: one function that posts to an operation with the headers above and turns refusals (`{ code, message, fieldErrors }`) into typed errors.
2. store.get on each request; render according to access, publicPrices, guestCheckout, fulfillment and currency.
3. Catalog and product pages with the catalog operations on the flows page.
4. Buyers: sign in with POST https://apis.kentro.dev/auth/login/customer `{ email, password, host }`, sign out with POST /auth/logout, sign up with session.signup, and call account.confirm when session.get answers confirmed: false. Guests: session.guest when store.get answers guestCheckout: true.
5. Cart: cart.get, cart.add, cart.set, cart.remove, and cart.update for buyers or cart.contact for guests, plus the cart choice after sign-in when both carts hold items.
6. Quote and checkout: cart.get `{ "quote": true }`, then checkout.start. When state is pending_payment or needs_action, send the buyer to redirectUrl and call checkout.confirm when they return; otherwise poll checkout.status.
7. Orders: orders.list and orders.get.
8. On every refusal, show the buyer the message from the answer; it is written for buyers.
Acceptance checks. Run each one against the sandbox and show the user the requests and responses, with secrets redacted:
- store.get answers 200 for the host; a request without X-Storefront-Host answers 400 VALIDATION.
- A cart.add with a stale expectedRevision answers 409 CONFLICT, and the application re-reads the cart instead of failing.
- cart.get `{ "quote": true }` answers quoteReady: true with a quoteFingerprint for a cart with an address and a payment method.
- checkout.start reaches pending_payment or complete; sending it again with the same idempotencyKey does not create a second order, and checkout.status `{ idempotencyKey, quoteFingerprint }` finds the attempt.
- The signed-in buyer sees the new order in orders.list.
- The production build's client assets contain neither the API key's secret nor the string X-API-Key.
Updated about 12 hours ago

