API v2 is available via spree_legacy_api_v2 gem and will work with Spree 5. However new features such as Markets or new Pricing engine are only available in API v3.
TL;DR
Mental model: what changed and why
v2 — JSON:API with action endpoints
Storefront v2 was modelled on JSON:API. Every response haddata/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.
v3 — REST with explicit resources
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_…). YouPATCH /carts/:idto attach an email or addresses, and youPOST /carts/:id/itemsto add line items. - Delivery rates and payment methods are not separate endpoints — fulfillments are nested under the cart (
PATCH /carts/:id/fulfillments/:fidto pick a delivery rate), and payments are nested under the cart (POST /carts/:id/paymentsfor non-session methods,POST /carts/:id/payment_sessionsfor Stripe/PayPal/Adyen). - Completing checkout is a single explicit call:
POST /carts/:id/complete. It returns the resultingOrder.
Why JSON:API is gone
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 whenexpand is requested:
?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.
Prefixed IDs everywhere
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.
SDK: @spree/storefront-api-v2-sdk → @spree/sdk
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.
Installing
Creating a client
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.
Calling an endpoint
Auth tokens
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.
Error handling
In v3 the SDK throws aSpreeError 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.
TypeScript types
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.Endpoint mapping
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).
Catalog: products, taxons, categories
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.
Cart and checkout
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.
The new RESTful design allows you to implement different usage scenarios like multiple saved carts per customer or organization (company).
Session-based payments (Stripe, Adyen, PayPal)
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 teams to deliver payment integrations faster. Also your frontend code doesn’t need to change per gateway.
Customer account
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).
Geography and store metadata
Wishlists
Digital downloads
Removed without a v3 equivalent
A handful of v2 surfaces don’t exist in v3:- Posts / Menus / CMS Pages — the blog/CMS surface is not part of v3 or Spree Core anymore. Recommended: use a dedicated CMS like Payload or Strapi
Migration checklist
The mechanical bits, in order:- Install
@spree/sdkalongside@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 })withcreateClient({ 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 didif (response.isSuccess()) { response.success() }becomes a single statement, with errors thrown asSpreeError. - Update token handling. Replace
{ bearer_token, order_token }per-method arguments with the{ token, spreeToken }second-argumentRequestOptions. JWT tokens come fromclient.auth.login/client.customers.create; cart tokens come fromcart.tokenon the cart resource. - Convert filters from
filter[...]toq[...]. Most filters have a direct Ransack equivalent (see the Querying reference). For products specifically,taxon_ids→in_categories,name→name_contorsearch,pricerange →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
completeat the end. - Stop walking
included. Replace JSON:API normalization helpers with direct attribute access. Useexpandto 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/tokenendpoints 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, andlogoutrevokes the token server-side.

