Authentication

The API key, the buyer token, the guest token, the headers, token lifetimes, password reset, sign-up and the tenant guard.

A Store API request carries up to three identities. The API key says which storefront server is calling. The storefront host says which store it serves. The buyer token or guest token says whom it is acting for. Every operation needs the first two; most need the third.

IdentityCredentialWhere it comes fromWho holds it
The storefront serverX-API-KeyUsers > API keys in Kentro OS, with a permission group holding storefront:serveThe server only
The buyerX-Storefront-Buyer: <accessToken>POST https://apis.kentro.live/auth/login/customerThe server, keyed by the browser's session cookie
The guestX-Storefront-Guest: <guestToken>POST /session.guestThe server, keyed by the browser's session cookie

A request with no buyer and no guest token is a signed-out visitor: it can read store.get, and the catalog when store.get answers access: "public". Everything else answers 401 UNAUTHORIZED with "Sign in to continue."

Headers

HeaderRequiredValue
X-API-KeyyesThe API key. Checked at Kentro's gateway, which resolves the tenant and the scopes.
X-Storefront-HostyesThe published storefront's host (shop.example.com), lower case, no scheme, no path. The store, its channel and its tenant are resolved from it.
X-Storefront-Buyerfor a signed-in buyerThe access token POST /auth/login/customer answered.
X-Storefront-Guestfor a guestThe guest token POST /session.guest answered. May be sent alongside X-Storefront-Buyer right after sign-in, so the guest's cart can be offered to the buyer (see carts across sign-in).
X-Storefront-Client-IprecommendedThe buyer's own IP address, one address, as the storefront server saw it. Keys per-visitor limits (the search typeahead, sign-up and reset limits) to the buyer instead of the storefront server. Ignored when it is not exactly one IP address.
X-Expected-PortaloptionalThe storefront id (id from store.get). When the host now resolves to a different storefront, the answer is 409 CONFLICT "Your store or account changed. Reload before continuing."
X-Expected-BuyeroptionalThe buyer id the server believes the token belongs to, guest:<guestId> for a guest, or guest for no identity. A mismatch is the same 409 CONFLICT. Sends a stale browser back to the start instead of acting for the wrong account.
Content-Typeyesapplication/json. Every operation takes a JSON body, {} when it takes no fields.

Authorization is not read. The buyer token travels in X-Storefront-Buyer, never as a bearer token.

The buyer: sign-in, refresh, sign-out

Buyer sessions live on Kentro's authentication service at https://apis.kentro.live/auth (sandbox: https://apis.kentro.dev/auth). These calls take no API key; host in the body is the storefront host.

Sign in

curl -s -X POST https://apis.kentro.live/auth/login/customer \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "password": "********", "host": "shop.example.com" }'
{
  "accessToken": "eyJhbGciOi...",
  "refreshToken": "4f2c7a...",
  "expiresIn": 900,
  "user": { "id": "7d2a...", "email": "[email protected]", "name": "Ada Lovelace", "tenantId": "0a1b...", "type": "customer" }
}

email is the buyer's email address or phone number, whichever the account signs in with. The storefront server keeps both tokens against the browser's session and sends accessToken as X-Storefront-Buyer. A wrong password answers 401 { "error": "INVALID_CREDENTIALS", "message": "..." }; repeated failures for one account are slowed down. A deactivated tenant answers 403 { "error": "TENANT_INACTIVE" }.

Signing in does not by itself grant purchasing. A buyer on a store with access: "approval" who has no approved access answers 403 APPROVAL_REQUIRED on the catalog, cart and account operations until staff approve them in Kentro OS; a buyer who still has to confirm the account answers 403 CONFIRMATION_REQUIRED. Read session.get after sign-in: it answers { buyerId, authenticated, email, name, confirmed } and tells the storefront which panel to show.

Lifetimes and refresh

TokenLifetimeNotes
Access token15 minutes (expiresIn: 900)Sent as X-Storefront-Buyer. An expired token answers 401 UNAUTHORIZED "Storefront authorization failed."
Refresh token30 days, rotatingEach refresh answers a new pair and retires the old refresh token.
Guest tokenUntil the guest cart is claimed, discarded or expiresSee the guest.

Refresh before the access token expires, or on the first 401:

curl -s -X POST https://apis.kentro.live/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "'"$REFRESH_TOKEN"'" }'
{ "accessToken": "eyJhbGciOi...", "refreshToken": "9c01e4...", "expiresIn": 900 }

A refresh token that was already rotated, revoked by sign-out, or invalidated by a password change answers 401; send the buyer to sign in again. A password change (reset by email or text message, or account.changePassword) ends every session of the account, this browser's included.

Sign out

curl -s -X POST https://apis.kentro.live/auth/logout \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "'"$REFRESH_TOKEN"'" }'

Answers { "success": true } and revokes the refresh token. Clear the browser's session cookie in the same response.

The guest

A store with guestCheckout: true lets a visitor build a cart and check out without an account. The guest is identified by a guest token the storefront server mints once per browser and stores against the browser's session:

POST /session.guest
X-API-Key: ...
X-Storefront-Host: shop.example.com
{}
{ "guestToken": "gst_..." }

Send it as X-Storefront-Guest on every call for that browser. A guest can read the catalog (prices only when guestPrices is true), fulfillment.locations, and use cart.*, cart.contact (the guest's email and addresses), and checkout.*. Account, address, payment profile, order history and invoice operations answer 401 UNAUTHORIZED for a guest; a guest reads the order it just placed through orders.get { id, guestAttempt: true } with the same guest token.

On a store with guestCheckout: false, session.guest is still answered but guest carts refuse to check out (403 CHECKOUT_DISABLED "Guest checkout is unavailable."). Mint the guest token only when store.get says guest checkout is on.

Sign-up and account confirmation

Sign-up follows the store's acquisition policy from store.get: methods (email, phone), requiredFields, inviteCodeIsEnabled, publicSignup, requireConfirmation and mode (public, open, approval, invite).

POST /session.signup
X-API-Key: ...
X-Storefront-Host: shop.example.com
X-Storefront-Client-Ip: 203.0.113.7
{
  "method": "email",
  "email": "[email protected]",
  "password": "********",
  "inviteCode": "TRADE-2026",
  "name": "Ada Lovelace",
  "company": "Analytical Engines Ltd",
  "customer": { "firstName": "Ada", "lastName": "Lovelace", "phone": "+1 415 555 0100" },
  "marketingOptIn": true
}

Two answers:

  • { "status": "confirmation-required", "message": "..." }: the store requires confirmation. The buyer received a 5-digit code; sign the buyer in, then call account.confirm.
  • { "status": "complete", "approved": true | false, "customerId": "..." }: the account exists. Sign the buyer in with the same credentials; approved: false means staff still have to approve access and purchasing operations answer 403 APPROVAL_REQUIRED until they do.

method is required (email or phone); method: "phone" sends phone instead of email; confirmationCode carries a code the buyer already has. Validation failures answer 400 VALIDATION with fieldErrors keyed by field; a duplicate account answers 409 CONFLICT; a body with a field the schema does not name answers 400 VALIDATION "Invalid signup input." Sign-up is limited per source address (429 RATE_LIMITED), which is why X-Storefront-Client-Ip matters here. session.signup, account.confirm and account.changePassword relay the authentication service's answer as is, status and fieldErrors included.

A signed-in buyer who still has to confirm (session.get answers confirmed: false) confirms through account.confirm, with the buyer token:

{ "confirmationCode": "48213" }

An empty body {} asks for a new code and answers { "status": "confirmation-required", "message": "...", "destination": "a***@example.com" }; a correct code answers { "status": "complete" }. Until the account is confirmed, catalog.* (except catalog.categories), cart.*, checkout.*, orders.* and invoices.* answer 403 CONFIRMATION_REQUIRED "Confirm your account to continue."

A signed-in buyer changes the password through account.changePassword:

{ "currentPassword": "********", "password": "a-new-passphrase" }

Answers { "message": "..." } and ends every session of the account; a wrong current password answers 400 VALIDATION with fieldErrors.currentPassword. The new password is at least 8 characters.

Password reset

Both flows are on the authentication service, take no API key, and answer the same body whether or not an account exists, so they reveal nothing about which addresses or numbers have accounts.

By email

POST https://apis.kentro.live/auth/password-reset/customer/request
{ "host": "shop.example.com", "email": "[email protected]" }
{ "result": true, "message": "If the email matches an active storefront account, a reset link will be sent." }

The email carries a link with a single-use token. The storefront's reset page sends it back with the new password:

POST https://apis.kentro.live/auth/password-reset/customer/confirm
{ "host": "shop.example.com", "token": "<token from the link>", "password": "a-new-passphrase" }
{ "result": true, "message": "If the link is valid, the storefront password was updated.", "updated": true }

updated: false means the link had expired, was already used, or the password changed since; offer a new request.

By text message

POST https://apis.kentro.live/auth/password-reset/customer/phone/request
{ "host": "shop.example.com", "phone": "+14155550100" }
{ "result": true, "message": "...", "smsAvailable": true }

The buyer receives a 5-digit code that works for 10 minutes, once. smsAvailable: false means this deployment cannot text; offer the email link instead. store.get announces the same fact as acquisition.smsAvailable.

POST https://apis.kentro.live/auth/password-reset/customer/phone/confirm
{ "host": "shop.example.com", "phone": "+14155550100", "code": "48213", "password": "a-new-passphrase" }
{ "result": true, "message": "...", "updated": true }

updated: false is answered alike for a wrong, expired, used or never-issued code. A changed password ends every session for the account.

The tenant guard

The tenant of a request is never read from the request. The gateway resolves it from the API key; the storefront host is resolved to its storefront, and the two must agree. A key made in tenant A sending the host of tenant B's storefront answers 403 FORBIDDEN "This storefront belongs to another account." Nothing in a body, a query string or a header selects a tenant.

What the gateway and the verifier answer

Failures before the request reaches a store operation:

StatusBodyCause
401{ "error": "INVALID_REQUEST", "message": "Missing API key credentials" }No X-API-Key.
401{ "error": "INVALID_CREDENTIALS", ... }Unknown, revoked or expired key.
403{ "error": "TENANT_INACTIVE", ... }The key's tenant is deactivated.
403{ "code": "FORBIDDEN", "message": "This API key cannot serve a storefront." }The key's groups hold neither storefront:serve nor admin.
400{ "code": "VALIDATION", "message": "X-Storefront-Host is required: the published storefront host." }X-Storefront-Host missing or longer than 253 characters.
404{ "code": "NOT_FOUND", "message": "No published storefront answers this host." }No live storefront on that host.
403{ "code": "FORBIDDEN", "message": "This storefront belongs to another account." }The host belongs to another tenant.
401{ "code": "UNAUTHORIZED", "message": "Storefront authorization failed." }The buyer or guest token is expired, revoked or not this store's. Refresh, or start a new guest.
409{ "code": "CONFLICT", "message": "Your store or account changed. Reload before continuing." }X-Expected-Portal or X-Expected-Buyer mismatch.
503{ "code": "UNAVAILABLE", "message": "Authentication is temporarily unavailable." }Retry with backoff.

The first three use the gateway's own shape (error); the rest use the Store API's error envelope.

Keep on the server

  • The API key, buyer tokens and guest tokens never reach the browser. The browser holds the storefront's own session cookie; the server maps it to the tokens.
  • Send X-Expected-Buyer on every call with the buyer id the session cookie maps to, so a browser whose session changed under it is answered 409 instead of acting on another account's cart.
  • Resolve nothing tenant-related from the browser. The host is the storefront's own host; the tenant follows from the key.