Migrating from the legacy Store API

What changed, call by call, between api.kentro.live/v1/store/* and the Store API.

The legacy Store API (https://api.kentro.live/v1/store/*) keeps answering for the storefronts that use it, but it is deprecated: it receives no new fields or endpoints, its reference is no longer published, and it is retired once every storefront has moved. New storefronts use the Store API at https://apis.kentro.live/store/v1/*; existing ones migrate with this page, whose call-by-call table is the record of what each legacy call did.

What changed

LegacyStore API
Host api.kentro.live, REST paths with GET and POST (GET /v1/store/order/list, POST /v1/store/cart/add)Host apis.kentro.live, every call POST /store/v1/<family>.<operation> with a JSON body (conventions).
The store was resolved from the request's Origin or Host header, or X-Host-Override on the sandbox; browsers could call it directlyThe store is X-Storefront-Host; the caller is the storefront server with an API key (X-API-Key) that must hold storefront:serve. Browsers never call the API.
A Feathers session: a cookie set by POST /v1/store/login, or its access token in AuthorizationA buyer access token from POST https://apis.kentro.live/auth/login/customer, sent as X-Storefront-Buyer, refreshed with POST /auth/refresh; guests get a token from session.guest and send X-Storefront-Guest.
No concurrency control on the cartrevision and ownershipVersion on every cart answer; expectedRevision on every mutation; stale is 409 CONFLICT.
Checkout in one call (cart/checkout) with the payment intent created separatelyA quote (cart.get { quote: true } with quoteFingerprint), then checkout.start with an idempotency key, then checkout.status and checkout.confirm around the provider.
Query parameter ?idempotency=idempotencyKey (or requestId) in the body where an operation takes one.
Amounts as integers in minor units, plus totals-beautify for formatted stringsIntegers in minor units everywhere; no formatted strings. Format on your side with currency.
Feathers errors ({ name, message, code, className, data, errors })One envelope, { code, message, fieldErrors }, with a stable code list and buyer-ready messages.
Addresses and payment profiles as free-form recordsBooks with version; writes send expectedVersion; the answer is the whole book.
Signup, password reset and login on the store pathsLogin, refresh, logout and password reset on apis.kentro.live/auth; sign-up, guest tokens, account confirmation and password change as Store API operations.

What stays the same

  • The data. Both APIs read and write the same storefront, buyers, carts, orders and invoices. A buyer created through the legacy API signs in through POST /auth/login/customer with the same credentials; orders placed through either API appear in orders.list.
  • The storefront host. The host the legacy API resolved the store from is the X-Storefront-Host the new one takes.
  • Buyer approval. Stores that required approval still do; an unapproved buyer answers 403 APPROVAL_REQUIRED instead of an empty catalog.

Call by call

Legacy paths are relative to https://api.kentro.live/v1/store; replacements are Store API operations relative to https://apis.kentro.live/store/v1, or authentication service routes relative to https://apis.kentro.live/auth.

Session

Legacy callReplacement
POST signupPOST /session.signup, then sign in; POST /account.confirm for the confirmation code
GET login (is the session authenticated)POST /session.get
POST loginPOST /auth/login/customer { email, password, host }; the answer's accessToken is the buyer token
GET logoutPOST /auth/logout { refreshToken }
POST password/resetPOST /auth/password-reset/customer/request { host, email }; by text message POST /auth/password-reset/customer/phone/request { host, phone }
POST password/updatePOST /auth/password-reset/customer/confirm { host, token, password }; by text message .../phone/confirm { host, phone, code, password }

Catalog

Legacy callReplacement
GET product/listPOST /catalog.list
GET product/details/{productId}POST /catalog.get { id, selector: "id" } (or by handle with selector: "handle")
GET product/categoriesPOST /catalog.categories
GET product/category-details/{categoryId}No replacement. catalog.categories carries each category's id, name, handle, parentId, image, description and productCount; catalog.list { category, categoryNamespace } lists its products.
GET product/tagsNo replacement. Tags the store shows come on each product as tags.
GET product/brandsNo replacement. The brand comes on each product as brand.

Cart

Legacy callReplacement
POST cart/detailsPOST /cart.get {}
POST cart/itemsPOST /cart.get {}: lines
POST cart/items-countPOST /cart.get {}: itemCount
POST cart/items-count-uniquePOST /cart.get {}: lines.length
POST cart/addPOST /cart.add; several lines at once POST /cart.addMany
POST cart/updatePOST /cart.set { lineId, quantity }
POST cart/removePOST /cart.remove { lineId }
POST cart/coupon-code-applyPOST /cart.update { coupon }
POST cart/coupon-code-removePOST /cart.update { coupon: "" }
POST cart/referral-code-applyPOST /cart.update { referralCode }
POST cart/referral-code-removePOST /cart.update { referralCode: "" }
POST cart/totalsPOST /cart.get { quote: true }
POST cart/totals-beautifyNo replacement. Format the integer amounts with the cart's currency on your side.
POST cart/get-shipping-ratesPOST /cart.get { quote: true }: shippingRates
POST cart/set-shipping-ratePOST /cart.update { shippingRateId }
POST cart/get-fulfillment-zonesPOST /fulfillment.locations { postalCode, country } or { searchLocation }
POST cart/set-fulfillment-zonesPOST /cart.update { fulfillment, locationId, lineLocations }
POST cart/update-shopping-geo-locationPOST /cart.update { searchLocation: { lat, lng } }
POST cart/update-shippingBuyers: POST /addresses.save then POST /cart.update { shipToId }; guests: POST /cart.contact
POST cart/update-billingBuyers: POST /addresses.save then POST /cart.update { billToId }; guests: POST /cart.contact
POST cart/can-checkoutPOST /cart.get { quote: true }: checkoutAllowed and issues
POST cart/create-payment-intentFolded into POST /checkout.start: its answer carries clientSecret or redirectUrl; Authorize.net card entry is configured by POST /payments.cardConfig
POST cart/checkoutPOST /cart.get { quote: true }, then POST /checkout.start, then POST /checkout.status and POST /checkout.confirm

Orders

Legacy callReplacement
GET order/listPOST /orders.list { page, status, search, from, to }
GET order/details/{orderId}POST /orders.get { id }; POST /orders.reorder { id } to buy it again

Carriers

Legacy callReplacement
GET carrier/list-allPOST /carriers.book {}: options
GET carrier/list-personalPOST /carriers.list {} (or carriers.book: entries)
POST carrier/addPOST /carriers.save without id
POST carrier/updatePOST /carriers.save { id, expectedVersion, ... }; POST /carriers.default { id, expectedVersion } to make it the default
POST carrier/removePOST /carriers.remove { id, expectedVersion }

Account

Legacy callReplacement
GET account/detailsPOST /account.get {}
POST account/updatePOST /account.update { expectedVersion, ... }; the password through POST /account.changePassword
GET account/creditsPOST /account.get {}: creditBalance, referralCredit, availableCredit, creditLimit, balanceDue
GET account/address/listPOST /addresses.list {}
POST account/address/addPOST /addresses.save without id (createId for an idempotent retry)
POST account/address/updatePOST /addresses.save { id, expectedVersion, ... }
POST account/address/removePOST /addresses.remove { id, expectedVersion }
GET account/payment/list-profilesPOST /payments.list {}
POST account/payment/add-profilePOST /payments.save (Authorize.net: after POST /payments.cardConfig; Stripe: follow redirectUrl, then POST /payments.confirm); POST /payments.status to poll
POST account/payment/update-profilePOST /payments.save { id, expectedVersion, ... }; POST /payments.default { id, expectedVersion }
POST account/payment/remove-profilePOST /payments.remove { id, expectedVersion }
GET account/price-listNo replacement. Every catalog and cart answer is already priced at the buyer's price list; account.get names it (priceList.name).

Subscriptions

Legacy callReplacement
GET subscription/list-plansPOST /subscription.list-plans {}
GET subscription/details/{planId}POST /subscription.plan-details { id }
GET subscription/activePOST /subscription.active {}
POST subscription/updatePOST /subscription.update
POST subscription/update-itemsPOST /subscription.update-items { action, ... }
POST subscription/activatePOST /subscription.activate
POST subscription/cancelPOST /subscription.cancel
POST subscription/subscribePOST /subscription.subscribe { planId, selectedFrequency }
POST subscription/skipPOST /subscription.skip
POST subscription/pausePOST /subscription.pause
POST subscription/resumePOST /subscription.resume
GET subscription/historyPOST /subscription.history {}

Every subscription change takes expectedRevision and requestId (flows).

Extras and applications

Legacy callReplacement
GET extra/countriesPOST /addresses.options {}: countries
GET extra/provinces/{alpha2}POST /addresses.options { country }: regions
POST extra/address-validationNo replacement. Validate with your own geocoder and send the result as geoLocation on addresses.save; delivery eligibility is answered by fulfillment.locations and the cart's deliveryEligible.
POST signup-applicationPOST /application.start, application.upload.permit, application.upload.put, application.upload.confirm, POST /application.submit

Behavior the new API refuses on purpose

  • A cart mutation with a stale expectedRevision: 409 CONFLICT. The legacy API applied the last write.
  • A checkout without a current quote, or with a quoteFingerprint that no longer matches: 409 CONFLICT. The legacy API re-priced silently at checkout.
  • A second checkout.start while one is in flight: 409 CHECKOUT_PENDING. Poll checkout.status with the first key.
  • A write to an address, payment method, carrier account or account profile without its current expectedVersion: 409 CONFLICT.
  • A product group named instead of its variant: 409 PRODUCT_VARIANT_REQUIRED.
  • A guest reading account, address, payment, order history or invoice operations: 401 UNAUTHORIZED.

A migration plan that has worked

  1. In Kentro OS create an API key for the storefront server with a permission group holding storefront:serve. Keep the legacy storefront running.
  2. Move the reads first: store.get, catalog.*, session.get, orders.list, orders.get, account.get, addresses.list, payments.list. Reads have no side effects and show the new shapes, prices in minor units included.
  3. Move sign-in to POST /auth/login/customer and keep the two tokens server-side against your session cookie; add the refresh on 401.
  4. Move the cart, sending expectedRevision from the last answer on every mutation and rendering the cart the API answers.
  5. Move checkout: quote, checkout.start with an idempotency key, checkout.status and checkout.confirm. Test a decline and a lost response on the sandbox.
  6. Move account writes, carriers, subscriptions and applications; retire the legacy calls; tell [email protected] the storefront has moved so its legacy traffic can be confirmed at zero.

Questions during a migration go to [email protected] with the message and the request's X-Request-Id response header, when present, of any refusal.