Use this file to discover all available pages before exploring further.
The Store API is the successor to the legacy Storefront API. It exposes the same surface — products, carts, checkout, customers, wishlists — but with a new transport (flat JSON instead of JSON:API), a new fully-typed TypeScript SDK, and resource-oriented routes that line up with the new Admin API. It’s faster, safer and easier to work with.This guide walks you through the differences and gives you a one-to-one mapping you can grep against during a migration.
Storefront v2 was modelled on JSON:API. Every response had data/attributes/relationships/included, and most non-GET calls invoked a named action on a singleton resource (/cart/add_item, /checkout/next, /checkout/select_shipping_method). The current cart was implicit — the server resolved it from the X-Spree-Order-Token header. Checkout was a five-step state machine; the SPA’s job was to drive PATCH /checkout/next until the order reached complete.This made cart and checkout calls easy to write but hard to reason about. The same payload could land you in different checkout states depending on which step the order happened to be on, and refactoring the front-end meant knowing which actions transitioned which states.
Store API v3 collapses checkout into the cart. There is no checkout state machine, no /checkout/next, no /checkout/advance. Instead:
The cart is a real resource with a stable prefixed ID (cart_…). You PATCH /carts/:id to attach an email or addresses, and you POST /carts/:id/items to add line items.
Delivery rates and payment methods are not separate endpoints — fulfillments are nested under the cart (PATCH /carts/:id/fulfillments/:fid to pick a delivery rate), and payments are nested under the cart (POST /carts/:id/payments for non-session methods, POST /carts/:id/payment_sessions for Stripe/PayPal/Adyen).
Completing checkout is a single explicit call: POST /carts/:id/complete. It returns the resulting Order.
The cart can be created, edited, abandoned, completed, and associated with a user from a single URL. No step transitions, no implicit current cart. This makes it possible to ship Shopify-style one-page checkout without fighting the API.
JSON:API’s strengths — sparse fieldsets, relationship graphs, normalized payloads — are real, but most storefront clients flattened the response anyway. The cost was a noisy, hard-to-cache wire format and a two-step deserialization on every call. v3 returns the resource directly with associations inlined when expand is requested:
You can still ask for sparse fields (?fields=name,price), and you still control association depth (?expand=variants.media), but you no longer have to walk included to assemble the response. See Querying and Relations.
Every v3 resource has a Stripe-style prefixed ID — prod_…, variant_…, cart_…, ord_…, addr_…. The prefix is part of the public surface: pass it back exactly as received, never strip the prefix or cast it to an integer. (Internally, IDs are still numeric, but the API only ever exposes the prefixed form.) See the Introduction.
The two SDKs cover the same ground but differ in shape. The legacy @spree/storefront-api-v2-sdk uses a makeClient factory, exposes resource namespaces (account, cart, checkout, products, taxons, wishlists), wraps every response in a Result<Error, Response> envelope, and passes tokens via an IToken ({ orderToken, bearerToken }) argument on every method.@spree/sdk uses a createClient factory, lines its resource namespaces up with the REST tree (products, categories, carts, carts.items, customer.orders, …), returns the resource directly (no Result wrapper), and threads auth through a per-call RequestOptions ({ token, spreeToken }) — the publishable key is set once at client construction.
The Storefront API v2 had no API key concept — anyone with the host could call it. Store API v3 introduces a publishable key (pk_xxx) that’s required on every request and identifies which store the call targets. The key is safe to expose in client-side code; it’s how v3 supports multi-store on a single domain and gives you per-key rate limits, scopes, and audit trails.
In v3, token and spreeToken are passed via the RequestOptions object on each call — no more per-method bearer_token / order_token arguments mixed into the body. JWT refresh uses client.auth.refresh({ refresh_token }); the old OAuth refresh_token grant against /spree_oauth/token is gone.
In v3 the SDK throws a SpreeError instance with code, status, and details properties. Wrap calls in try/catch or let them bubble. The Result<Error, Response> wrapper from v2 is gone — code that branched on response.isSuccess() becomes a single happy path plus a catch.
v3 ships generated TypeScript types and runtime Zod schemas that stay in lockstep with the API — every response field is typed, and you can validate payloads at runtime where you need belt-and-braces safety (form submissions, untrusted webhooks). v2’s types were hand-maintained interfaces inside the SDK, which drifted from the actual responses over time.
The tables below cover every public path in /api/v2/storefront/* and where to find its v3 equivalent. Anything not listed is unchanged in scope but follows the new conventions (flat JSON, prefixed IDs, Ransack filters).
Filters move from filter[...] to Ransack q[...]; include → expand.
GET /products/:slug
GET /products/:id_or_slug
Accepts prefixed ID or slug.
GET /products/:slug/variants
GET /products/:id?expand=variants
Variants are returned via expand, not as a separate route.
GET /taxons
GET /categories
Renamed. v3 calls them Categories everywhere — same tree model, same permalink, parametrised by slug or prefixed ID.
GET /taxons/:id
GET /categories/:id_or_permalink
Permalinks containing slashes (clothing/shirts) work as-is.
(new)
GET /products/filters
Returns price range, in-stock toggle, option values, and category facets with counts — designed for filter sidebars.
This is an area where API v3 has the biggest performance advantage over v2. GET /products by default will expose default_variant_id, thumbnail_url and price which are essential for building product lists. You don’t need to expand variants or media (images) like with API v2.
This is the biggest conceptual change. The v2 cart was a singleton accessed via the order token header; v3 carts have prefixed IDs and live alongside line items, payments, fulfillments, and discount codes as nested resources. There is no checkout state machine in v3. Backend will handle that automatically, without any developer action needed. This aligns with Spree 6 upcoming changes.By default all Cart endpoints will return all associations auto-expanded.
Storefront API v2
Store API v3
Notes
POST /cart
POST /carts
Returns a Cart with a prefixed id and a token (use as X-Spree-Token for guests).
GET /cart
GET /carts/:id
Pass the prefixed id. Authenticated users can GET /carts to list active carts.
DELETE /cart
DELETE /carts/:id
Same semantics.
POST /cart/add_item
POST /carts/:id/items
Nested resource, not an action.
PATCH /cart/set_quantity
PATCH /carts/:id/items/:line_item_id
Updates an explicit line item by ID.
DELETE /cart/set_quantity
DELETE /carts/:id/items/:line_item_id
Same.
PATCH /cart/empty
Iterate DELETE /carts/:id/items/:line_item_id
No bulk-empty action; remove line items individually, or DELETE /carts/:id to abandon.
PATCH /cart/apply_coupon_code
POST /carts/:id/discount_codes
Body: { code }.
DELETE /cart/apply_coupon_code
DELETE /carts/:id/discount_codes/:code
Path-level code.
DELETE /cart/remove_coupon_code
Iterate DELETE /carts/:id/discount_codes/:code
No remove-all shortcut; remove each code.
GET /cart/estimate_shipping_rates
Inspect cart.fulfillments[].delivery_rates
Rates are returned inline with the cart. Add an address (PATCH /carts/:id) and the cart is recomputed.
PATCH /cart/associate
PATCH /carts/:id/associate
Pass the JWT for the now-authenticated user.
PATCH /cart/change_currency
PATCH /carts/:id
Set currency directly on the cart.
PATCH /checkout
PATCH /carts/:id
Email, addresses, special instructions, etc. — all on the cart.
PATCH /checkout/next
(removed)
No state machine; nothing to advance.
PATCH /checkout/advance
(removed)
Same.
PATCH /checkout/complete
POST /carts/:id/complete
Returns the resulting Order.
PATCH /checkout/select_shipping_method
PATCH /carts/:id/fulfillments/:fulfillment_id
Body: { selected_delivery_rate_id }. ShippingMethod is the legacy term — v3 calls them Delivery Methods / Delivery Rates.
POST /checkout/validate_order_for_payment
(removed)
Validation happens server-side when you call complete.
POST /checkout/create_payment
POST /carts/:id/payments
For offline / non-session methods (cash, check, bank transfer).
POST /checkout/add_store_credit
POST /carts/:id/store_credits
Body: { amount? }.
POST /checkout/remove_store_credit
DELETE /carts/:id/store_credits
Same.
GET /checkout/payment_methods
cart.available_payment_methods
Inlined on the cart.
GET /checkout/shipping_rates
cart.fulfillments[].delivery_rates
Same — inlined.
New RESTful design allows to implement different usage scenarios like multiple saved carts per customer or organization (company).
API v2 had per-gateway endpoints (/stripe/payment_intents, /adyen/payment_sessions). API v3 unifies these behind a generic Payment Sessions API — the gateway-specific payload moves into the request body, and Spree dispatches to the right provider based on the payment_method_id. This shortens the integration time and allows team to deliver payment integrations faster. Also your frontend code don’t need to change per gateway.
Storefront API v2
Store API v3
POST /stripe/payment_intents
POST /carts/:id/payment_sessions
GET /stripe/payment_intents/:id
GET /carts/:id/payment_sessions/:id
PATCH /stripe/payment_intents/:id
PATCH /carts/:id/payment_sessions/:id
PATCH /stripe/payment_intents/:id (confirm)
PATCH /carts/:id/payment_sessions/:id/complete
POST /stripe/setup_intents
POST /customers/me/payment_setup_sessions (save card for future use)
API v2 exposed a singleton /account endpoint with OAuth tokens minted at /spree_oauth/token. API v3 splits the surface into a public registration endpoint (POST /customers) and a /customers/me namespace for the authenticated customer. Auth moves from OAuth to JWT (POST /auth/login).
Storefront API v2
Store API v3
Notes
POST /account
POST /customers
Returns JWT tokens on success.
GET /account
GET /customers/me
PATCH /account
PATCH /customers/me
current_password required to change email or password.
GET /account/addresses
GET /customers/me/addresses
POST /account/addresses
POST /customers/me/addresses
PATCH /account/addresses/:id
PATCH /customers/me/addresses/:id
DELETE /account/addresses/:id
DELETE /customers/me/addresses/:id
GET /account/credit_cards
GET /customers/me/credit_cards
GET /account/credit_cards/default
GET /customers/me/credit_cards?q[default_eq]=true
Use a Ransack filter; there’s no /default shortcut.
DELETE /account/credit_cards/:id
DELETE /customers/me/credit_cards/:id
GET /account/orders
GET /customers/me/orders
GET /account/orders/:number
GET /customers/me/orders/:id
Use prefixed ID or order number.
GET /order_status/:number
GET /orders/:id
Guest-accessible with the order token; no separate status endpoint.
(POST /spree_oauth/token grant=password)
POST /auth/login
Returns a JWT, not an OAuth token.
(POST /spree_oauth/token grant=refresh_token)
POST /auth/refresh
(none)
POST /auth/logout
Server-side revocation of the refresh token.
(none)
POST /password_resets / PATCH /password_resets/:token
Install @spree/sdk alongside @spree/storefront-api-v2-sdk. They have different package names, so both can coexist while you cut over endpoints incrementally.
Create a publishable API key in Spree Admin → Settings → API Keys (or via spree api-key create). v3 requires it on every request — v2 had no API key concept at all.
Replace makeClient({ host }) with createClient({ baseUrl, publishableKey }) in one entry point at a time. Keep the v2 client wired up for not-yet-migrated calls.
Switch from Result<…> to direct returns + try/catch. Any code that did if (response.isSuccess()) { response.success() } becomes a single statement, with errors thrown as SpreeError.
Update token handling. Replace { bearer_token, order_token } per-method arguments with the { token, spreeToken } second-argument RequestOptions. JWT tokens come from client.auth.login / client.customers.create; cart tokens come from cart.token on the cart resource.
Convert filters from filter[...] to q[...]. Most filters have a direct Ransack equivalent (see the Querying reference). For products specifically, taxon_ids → in_categories, name → name_cont or search, price range → price_gte / price_lte.
Rewrite cart/checkout calls as resource operations. This is the deepest change. The cleanest path is to delete your checkout step controller wholesale and rebuild it as a single page that PATCHes the cart and POSTs to nested resources, then calls complete at the end.
Stop walking included. Replace JSON:API normalization helpers with direct attribute access. Use expand to pull in associations, and accept that they arrive inlined.
Replace numeric IDs and slugs with prefixed IDs. Update any code that parsed integers out of IDs, stored IDs as numbers in state, or constructed admin links from raw IDs.
Switch the OAuth token endpoints for JWT. The /spree_oauth/token endpoints are no longer the customer auth surface; use /api/v3/store/auth/login / /auth/refresh / /auth/logout. Refresh tokens are rotated on each refresh call, and logout revokes the token server-side.
For the conceptual changes — flat JSON, RESTful checkout, Markets — give yourself a sprint of breathing room rather than treating the migration as a string-replace. The win on the other side is a smaller, more obvious client surface and a Store API that lines up with the Admin API for full-stack work.