Requests and responses

Operations and bodies, answer shapes, cart revisions, guest and buyer carts, money, the error envelope and its codes, caching and rate limits.

Every Store API operation follows the same rules. Learn them once here; the operation reference assumes them.

Operations, not resources

Every endpoint is POST /<family>.<operation> with a JSON body: POST /cart.add, POST /orders.get, POST /subscription.list-plans. Reads are POST too, with {} when they take no fields. The reasons:

  • Every call acts for one buyer named by headers, and most reads depend on that buyer (prices, visibility, carts), so nothing is cacheable by URL anyway.
  • Bodies carry what a storefront needs to send on a read (a location for stock, a quote request on the cart, a search), without encoding it in query strings.
  • The names are the names Kentro's own storefront templates use, so the operation reference, the templates and the migration table line up.

Bodies are checked against the schema printed on each operation. Send only the fields it names: an unknown field is refused with 400 VALIDATION on most operations and dropped on the rest; do not rely on either. Bodies are limited to 64 KB (application uploads excepted).

Answers

A successful answer is the object or array the operation describes, with no wrapper: store.get answers the store, cart.add answers the cart, addresses.list answers an array of addresses. Status is 200.

Three fields recur:

FieldOnMeaning
revision, ownershipVersionthe cartThe concurrency tokens every cart mutation sends back. See below.
version, expectedVersionaccount, addresses, saved payment methods, carrier accounts, subscriptionsA hash of the record as last read; sent back as expectedVersion (expectedRevision on subscriptions) so a write refuses to overwrite a change made elsewhere (409 CONFLICT).
idempotencyKey, requestIdcheckout, cart.addMany, cart claims, invoice payments, saved payment methods, subscriptions, applicationsAny string of 7 to 50 characters from A-Z a-z 0-9 _ - that names the request on your side. A retry with the same key answers the first attempt instead of acting twice.

Cart revisions

A cart answer carries revision (an integer that grows with every change) and ownershipVersion (a uuid that changes when the cart changes hands, for example when a guest cart becomes a buyer's cart). Every cart mutation (cart.add, cart.addMany, cart.set, cart.remove, cart.update, cart.contact) sends expectedRevision and may send ownershipVersion:

{ "expectedRevision": 7, "ownershipVersion": "c2f7...", "lineId": "9ad1...", "quantity": 3 }

When the cart's current revision is not expectedRevision, nothing is written and the answer is 409 CONFLICT with "Your cart changed. Review the current cart." Read cart.get, show the current cart, and let the buyer act again. Two tabs of one browser, or a buyer on two devices, resolve this way instead of silently overwriting each other.

cart.get { "quote": true } prices the cart (shipping, tax, credits, charge) and answers quoteFingerprint, a hash of everything the quote depends on. checkout.start sends it back with the expectedRevision; a cart or policy that changed in between is 409 CONFLICT and the quote is redone. A quote lives at most 10 minutes.

Guest and buyer carts

A guest token owns a guest cart; a buyer token owns the buyer's cart on this storefront. When a browser that holds a guest cart signs in, the server sends both X-Storefront-Buyer and X-Storefront-Guest on the next calls, and the API answers how the two carts stand:

  1. cart.choices {} answers { "guest": [...], "account": [...] }, each entry { id, ownershipVersion, retained, processing, adopted?, items: [{ name, quantity }] }.
  2. While both carts hold items and no choice was made, cart.* mutations answer 409 CART_CHOICE_REQUIRED "Choose which cart to continue with. Neither cart has been discarded."
  3. cart.choose { idempotencyKey, selectedId, guest: { id, ownershipVersion }, account: { id, ownershipVersion } | null } keeps selectedId as the buyer's cart and retains the other; it answers { selectedId, retainedIds }.
  4. cart.restore { id, ownershipVersion, idempotencyKey } brings a retained cart back; cart.discard { id, ownershipVersion, idempotencyKey } drops one and answers { discardedId }.

A retained cart that is read as if it were current answers 409 CART_RETAINED; a guest cart past its life answers 409 CART_EXPIRED. cart.get { "guestCart": true } reads the guest cart explicitly while both tokens are present. A cart that is mid-checkout cannot be transferred (409 CHECKOUT_PENDING): recover the checkout first.

adoption on a cart (and adopted on a choice) names a cart brought over from the store's previous platform; cart.update { "adoptionNoticeDismissed": true } records that the buyer saw the notice.

Money

Every amount is an integer in the minor unit of the store's currency: 1250 is 12.50 USD. This holds for product prices (price, compareAtPrice, priceRange), cart lines and totals (unitPrice, total, subtotal, shipping, tax, credits, charge, balanceDue, processingFee), orders, invoices, account credits and limits, shipping rates (amount) and subscription history. Inputs are integers too: expectedAmount on invoices.pay, expectedUnitPrice on cart.addMany. A derived amount (a percent deposit) is rounded half up to a whole minor unit on the server, so the amount shown, the amount charged and the amount recorded on the order are the same integer.

Prices answer null when the buyer may not see them (a signed-out visitor on a store without publicPrices, a guest without guestPrices); the cart answers pricesVisible: false and nulls its totals in that case.

Dates

Timestamps are ISO-8601 in UTC (2026-11-08T14:03:00.000Z). Calendar dates (from and to on orders.list, requestedDate on the cart) are YYYY-MM-DD.

Pagination

Lists are page based: page (1 to 500, default 1), a fixed pageSize, hasNextPage, and total with totalIsCapped: true when the count stopped at the cap. Catalog lists take pageSize (1 to 100) too.

Errors

Every refusal is JSON with one shape, status in the HTTP line:

{
  "code": "VALIDATION",
  "message": "Add an item before checking out.",
  "fieldErrors": {}
}

message is written for the buyer and can be shown as is. fieldErrors is keyed by field name when a specific field is at fault (sign-up, password change); otherwise {}.

StatuscodeMeaning
400VALIDATIONA field is missing, malformed, unknown, or the request makes no sense in the cart's state (empty cart at checkout, no payment method chosen). A few policy refusals use 422 with the same code (price sorting while prices are hidden).
401UNAUTHORIZEDNo buyer or guest where one is required ("Sign in to continue."), a token the store does not accept, or a guest operation without the original guest token.
403UNAUTHORIZEDA valid identity that may not do this: a guest reserving account credits, a cart whose owner changed.
403FORBIDDENThe API key cannot serve a storefront, or the host belongs to another tenant. Gateway level; see Authentication.
403APPROVAL_REQUIREDThe buyer is signed in but staff have not approved access to this store.
403CONFIRMATION_REQUIREDThe buyer must confirm the account (account.confirm) first.
403, 409CHECKOUT_DISABLEDCheckout is off for the store, or guest checkout or guest pricing is off for this cart.
404NOT_FOUNDNo published storefront on the host, or no order, invoice, address, payment method or checkout attempt with that id for this buyer.
404PRODUCT_NOT_FOUNDThe product is not in this buyer's catalog.
409CONFLICTA stale expectedRevision, expectedVersion or quoteFingerprint; an X-Expected-* mismatch; a checkout key reused for another quote; a checkout whose state moved on. Re-read and retry.
409CART_CONFLICTThe cart needs attention (two active carts, ownership unavailable). Contact the store.
409CART_CHOICE_REQUIRED, CART_RETAINED, CART_EXPIREDSee guest and buyer carts.
409IDENTITY_REQUIREDA guest checkout needs cart.contact (email and addresses) first.
409QUOTE_INCOMPLETEThe cart has no quote yet, or lacks a fulfillment location or payment method; cart.get { quote: true } after fixing the cart.
409QUOTE_PENDINGA shipping or tax quote is still being computed; retry cart.get { quote: true }.
409CHECKOUT_PENDINGA checkout is in progress on this cart; poll checkout.status.
409PAYMENT_PENDINGThe payment provider has not settled; poll checkout.status or payments.status.
409PRODUCT_VARIANT_REQUIREDThe product has variants; send variantId.
409INVALID_PRICE_LISTThe buyer's price list cannot price this cart. Contact the store.
429RATE_LIMITEDToo many catalog.suggest calls from this identity (Retry-After says when), or the sign-up and confirmation limits per account and source address on session.signup and account.confirm.
503UNAVAILABLEA dependency (authentication, payments, carrier or tax quotes) did not answer. Retry with backoff; a write that answered 503 was not applied unless a later read shows it.
503CAPABILITY_UNAVAILABLEThe store's configuration asks for something this API does not do (a legacy payment path). Contact the store.

A declined payment is not an error: checkout.* answers 200 with state: "declined" and a message; invoices.pay answers state: "declined" or "refused". An application rate limit (application.start 60 per minute, other application.* 120 per minute) answers 429.

Caching

Every answer carries Cache-Control: no-store. Answers depend on the buyer, the guest, the store's live configuration and stock, so the storefront server caches nothing from the Store API across requests. Build HTML caches, if any, from store.get and catalog.* read with no buyer token, and only for stores with access: "public"; a X-Store-Degraded response header marks an answer that must not be cached at all.

Rate limits

  • catalog.suggest (the search typeahead) is limited to 30 requests per 3 seconds per buyer, per guest, or per X-Storefront-Client-Ip for signed-out visitors. Over the limit: 429 RATE_LIMITED with Retry-After. Debounce the typeahead in the browser.
  • The gateway applies the per-key limits every Kentro API has. Sign-in and password reset are limited per account and per source address.

What the storefront server never does

  • Hold or send Kentro's internal service secret. A custom storefront has the API key, buyer tokens and guest tokens, nothing else; every operation it needs, including guest tokens and sign-up, is on the Store API or the public authentication service.
  • Trust a tenant id, a store id or a buyer id from the browser. The tenant comes from the key, the store from the host, the buyer from the token the server holds for that browser's session. X-Expected-Buyer is a check against the server's own session mapping, not a way to pick a buyer.
  • Call the Store API from the browser, or expose the API key in client-side code.
  • Reuse one guest token across browsers. One guest token is one visitor's cart.