@spree/docs 0.1.248 → 0.1.249

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/api-reference/admin-api/authentication.md +34 -14
  2. package/dist/api-reference/admin-api/endpoints.md +366 -14
  3. package/dist/api-reference/admin-api/errors.md +2 -2
  4. package/dist/api-reference/admin-api/introduction.md +3 -3
  5. package/dist/api-reference/admin-api/querying.md +6 -6
  6. package/dist/api-reference/store-api/monetary-amounts.md +5 -5
  7. package/dist/api-reference/webhooks-events.md +330 -335
  8. package/dist/developer/agentic/agent-skills.md +5 -2
  9. package/dist/developer/agentic/llm-docs.md +2 -1
  10. package/dist/developer/cli/admin-api.md +1 -1
  11. package/dist/developer/cli/quickstart.md +2 -2
  12. package/dist/developer/contributing/creating-an-extension.md +292 -146
  13. package/dist/developer/contributing/developing-spree.md +13 -17
  14. package/dist/developer/core-concepts/catalogs.md +2 -2
  15. package/dist/developer/core-concepts/channels.md +3 -3
  16. package/dist/developer/core-concepts/companies.md +2 -1
  17. package/dist/developer/core-concepts/delivery-setup.md +2 -2
  18. package/dist/developer/core-concepts/discounts.md +3 -3
  19. package/dist/developer/core-concepts/events.md +6 -5
  20. package/dist/developer/core-concepts/freight.md +3 -2
  21. package/dist/developer/core-concepts/fulfillments.md +10 -8
  22. package/dist/developer/core-concepts/imports-exports.md +11 -8
  23. package/dist/developer/core-concepts/inventory.md +2 -2
  24. package/dist/developer/core-concepts/media.md +14 -14
  25. package/dist/developer/core-concepts/orders.md +2 -2
  26. package/dist/developer/core-concepts/payments.md +1 -2
  27. package/dist/developer/core-concepts/products.md +6 -6
  28. package/dist/developer/core-concepts/reporting.md +4 -3
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +7 -7
  30. package/dist/developer/core-concepts/search-filtering.md +3 -3
  31. package/dist/developer/core-concepts/sellers.md +3 -3
  32. package/dist/developer/core-concepts/staff-roles.md +4 -2
  33. package/dist/developer/core-concepts/store-credits-gift-cards.md +1 -1
  34. package/dist/developer/core-concepts/stores.md +2 -2
  35. package/dist/developer/core-concepts/translations.md +12 -8
  36. package/dist/developer/core-concepts/webhooks.md +19 -18
  37. package/dist/developer/create-spree-app/quickstart.md +2 -7
  38. package/dist/developer/customization/api.md +1 -1
  39. package/dist/developer/customization/checkout.md +2 -2
  40. package/dist/developer/customization/dependencies.md +53 -37
  41. package/dist/developer/customization/permissions.md +2 -2
  42. package/dist/developer/dashboard/concepts.md +1 -1
  43. package/dist/developer/dashboard/customization/navigation.md +3 -2
  44. package/dist/developer/dashboard/customization/permissions.md +6 -6
  45. package/dist/developer/dashboard/plugins/publishing.md +4 -4
  46. package/dist/developer/dashboard/plugins/scaffolding.md +1 -1
  47. package/dist/developer/dashboard/public-api.md +1 -1
  48. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -18
  49. package/dist/developer/deployment/aws.md +1 -1
  50. package/dist/developer/deployment/aws_ecs.md +3 -3
  51. package/dist/developer/deployment/background_jobs.md +9 -3
  52. package/dist/developer/deployment/docker.md +1 -2
  53. package/dist/developer/deployment/emails.md +3 -1
  54. package/dist/developer/deployment/environment_variables.md +2 -2
  55. package/dist/developer/deployment/render.md +2 -2
  56. package/dist/developer/how-to/build-a-marketplace.md +2 -2
  57. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  58. package/dist/developer/how-to/custom-delivery-rate-provider.md +11 -3
  59. package/dist/developer/how-to/custom-document-numbers.md +1 -1
  60. package/dist/developer/how-to/custom-order-routing.md +15 -14
  61. package/dist/developer/how-to/custom-payment-method.md +17 -19
  62. package/dist/developer/how-to/custom-promotion.md +4 -4
  63. package/dist/developer/how-to/custom-search-provider.md +12 -5
  64. package/dist/developer/how-to/custom-stock-splitter.md +25 -24
  65. package/dist/developer/how-to/sell-digital-products.md +1 -1
  66. package/dist/developer/multi-tenant/quickstart.md +2 -2
  67. package/dist/developer/providers/payouts.md +6 -2
  68. package/dist/developer/sdk/admin/querying-and-errors.md +1 -1
  69. package/dist/developer/sdk/admin/quickstart.md +4 -4
  70. package/dist/developer/sdk/authentication.md +5 -2
  71. package/dist/developer/sdk/store/cart-checkout.md +4 -4
  72. package/dist/developer/storefront/nextjs/emails.md +4 -2
  73. package/dist/developer/storefront/nextjs/testing.md +1 -1
  74. package/dist/developer/upgrades/5.6-to-6.0.md +51 -20
  75. package/dist/integrations/search/meilisearch.md +4 -4
  76. package/package.json +1 -1
@@ -23,7 +23,7 @@ You need **Node.js 20+** to run the workspace scripts (including `pnpm server:se
23
23
 
24
24
  Spree is a monorepo with three main areas:
25
25
 
26
- - **`spree/`** — Ruby gems (core, api, admin, emails) distributed as separate packages via RubyGems
26
+ - **`spree/`** — Ruby gems (core, api, dashboard, emails, opentelemetry, and provider gems under `spree/providers/`) distributed as separate packages via RubyGems
27
27
  - **`packages/`** — TypeScript packages (SDKs, CLI, project scaffolding, docs, React dashboard)
28
28
  - **`server/`** — A Rails application cloned from [spree-starter](https://github.com/spree/spree-starter) that mounts the local Spree gems (not checked in — `pnpm server:setup` creates it)
29
29
 
@@ -38,9 +38,9 @@ pnpm server:setup # ~5–10 min on first run; idempotent
38
38
 
39
39
  `pnpm server:setup` clones [spree-starter](https://github.com/spree/spree-starter) into `./server/`, wires it to load Spree gems from the monorepo via a Docker compose overlay, builds the dev image, starts the stack (Postgres + Meilisearch + a single Rails `web` container — background jobs run in-process via Solid Queue), and prepares the database. The full sequence lives in `scripts/server-setup.sh`.
40
40
 
41
- When it's done, the backend is up at [http://localhost:3000](http://localhost:3000) and the admin is at [http://localhost:3000/admin](http://localhost:3000/admin). Sign in with the seed admin: **`spree@example.com`** / **`spree123`** (override at seed time with `ADMIN_EMAIL` / `ADMIN_PASSWORD` env vars — see `spree/core/app/services/spree/seeds/admin_user.rb`).
41
+ When it's done, the backend is up at [http://localhost:3000](http://localhost:3000). The React dashboard runs as its own development server — start it with `pnpm dashboard:dev` and open [http://localhost:5173](http://localhost:5173). The setup script prints a one-time link for creating the first admin account; open it once the dashboard is running. To seed an admin account instead, set the `ADMIN_EMAIL` and `ADMIN_PASSWORD` environment variables before seeding (see `spree/core/app/services/spree/seeds/admin_user.rb`).
42
42
 
43
- Optionally load sample products, taxonomies, and option types:
43
+ Optionally load sample products, categories, and option types:
44
44
 
45
45
  ```bash
46
46
  pnpm server:load_sample_data
@@ -110,8 +110,12 @@ The Spree [Rails engines](https://guides.rubyonrails.org/engines.html) live insi
110
110
  |---|---|---|
111
111
  | `core` | `spree_core` | Models, services, business logic |
112
112
  | `api` | `spree_api` | REST APIs |
113
- | `admin` | `spree_admin` | Admin dashboard |
114
- | `emails` | `spree_emails` | Transactional emails |
113
+ | `dashboard` | `spree_dashboard` | Serves a built React dashboard and seller panel from your Rails server |
114
+ | `emails` | `spree_emails` | Transactional emails (optional) |
115
+ | `opentelemetry` | `spree_opentelemetry` | OpenTelemetry tracing (optional) |
116
+ | `providers/stripe` | `spree_stripe` | Stripe payments (optional) |
117
+ | `providers/easypost` | `spree_easypost` | EasyPost delivery rates and shipping labels (optional) |
118
+ | `providers/meilisearch` | `spree_meilisearch` | Meilisearch product search (optional) |
115
119
 
116
120
  ### Spree namespace
117
121
 
@@ -133,7 +137,7 @@ bundle exec rake test_app
133
137
  bundle exec rspec
134
138
  ```
135
139
 
136
- Replace `core` with `api`, `admin`, or `emails` to test other engines.
140
+ Replace `core` with `api`, `dashboard`, `emails`, `opentelemetry`, or a provider path such as `providers/stripe` to test other engines.
137
141
 
138
142
  By default engine tests run against SQLite3. To run against PostgreSQL, set the `DB` environment variable:
139
143
 
@@ -181,15 +185,7 @@ bundle exec parallel_rspec -n 4 spec
181
185
 
182
186
  After schema changes, re-run `bundle exec rake parallel_setup` to update the worker databases.
183
187
 
184
- ### Integration tests (legacy Rails admin)
185
-
186
- The legacy Rails admin (`spree/admin`) ships feature specs that run in a real browser via chromedriver. You only need this if you're touching the legacy admin UI.
187
-
188
- Install chromedriver on macOS:
189
-
190
- ```bash
191
- brew install chromedriver
192
- ```
188
+ ### Integration tests
193
189
 
194
190
  The React dashboard (`packages/dashboard`) has its own end-to-end test suite running on Playwright against a real Rails backend — see [Dashboard E2E tests](#dashboard-e2e-tests) under TypeScript Development.
195
191
 
@@ -216,7 +212,7 @@ The backend setup is the same as for Ruby work — see [Setup](#setup) above. Wi
216
212
  | `@spree/cli` | `packages/cli` | yes | CLI for managing Spree Commerce projects |
217
213
  | `create-spree-app` | `packages/create-spree-app` | yes | Project scaffolding (`npm create spree-app`) |
218
214
  | `@spree/docs` | `packages/docs` | yes | Developer documentation for AI agents and local reference |
219
- | `@spree/dashboard` | `packages/dashboard` | no | React SPA admin dashboard (replaces the legacy Rails admin) |
215
+ | `@spree/dashboard` | `packages/dashboard` | no | React SPA admin dashboard |
220
216
  | `@spree/dashboard-core` | `packages/dashboard-core` | no | Framework: registries, providers, generic infra hooks for the dashboard |
221
217
  | `@spree/dashboard-ui` | `packages/dashboard-ui` | no | Design system used by the dashboard |
222
218
  | `@spree/sdk-core` | `packages/sdk-core` | no | Shared HTTP/retry/error layer used by both SDKs |
@@ -259,7 +255,7 @@ cd packages/dashboard
259
255
  pnpm dev # http://localhost:5173
260
256
  ```
261
257
 
262
- Sign in with `spree@example.com` / `spree123`. To point at a non-default backend, set `VITE_SPREE_API_URL` (also needs to be set at build time for production bundles, not just dev):
258
+ Sign in with the admin account you created from the setup link (see [Setup](#setup)). To point at a non-default backend, set `VITE_SPREE_API_URL` (also needs to be set at build time for production bundles, not just dev):
263
259
 
264
260
  ```bash
265
261
  VITE_SPREE_API_URL=https://my-spree.example.com pnpm dev
@@ -136,7 +136,7 @@ A [Channel](channels.md) is not assigned — it names its catalog directly via `
136
136
  ```typescript Admin SDK
137
137
  await adminClient.catalogs.assign(catalog.id, {
138
138
  assignable_type: 'customer_group',
139
- assignable_id: 'cgrp_xxx',
139
+ assignable_id: 'cg_xxx',
140
140
  })
141
141
  ```
142
142
 
@@ -144,7 +144,7 @@ await adminClient.catalogs.assign(catalog.id, {
144
144
  curl -X POST 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/assign' \
145
145
  -H 'X-Spree-API-Key: sk_xxx' \
146
146
  -H 'Content-Type: application/json' \
147
- -d '{ "assignable_type": "customer_group", "assignable_id": "cgrp_xxx" }'
147
+ -d '{ "assignable_type": "customer_group", "assignable_id": "cg_xxx" }'
148
148
  ```
149
149
 
150
150
 
@@ -38,7 +38,7 @@ erDiagram
38
38
  | `default` | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products | `true` |
39
39
  | `storefront_access` | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required` |
40
40
  | `guest_checkout` | Whether an order may be placed without an account on this channel. Unset inherits the store's setting | `false` |
41
- | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](fulfillments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
41
+ | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](../how-to/custom-order-routing.md) strategy | `Spree::OrderRouting::Strategy::Rules` |
42
42
 
43
43
  `code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.
44
44
 
@@ -105,7 +105,7 @@ Product status (`draft` / `active` / `archived`) is the **outer gate**: a Draft
105
105
 
106
106
  Every order is attributed to one channel. The channel is set from the `X-Spree-Channel` header on cart creation, from the merchant's selection on the "New order" form, or defaults to the store's primary channel.
107
107
 
108
- This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](fulfillments.md#order-routing).
108
+ This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](../how-to/custom-order-routing.md).
109
109
 
110
110
  ### Storefront Access Gating
111
111
 
@@ -232,7 +232,7 @@ The write contract is **full-set**: the array represents the complete desired st
232
232
  - [Stores](stores.md) — Channels belong to a store
233
233
  - [Markets](markets.md) — Different from channels: markets segment geography/currency, channels segment selling surfaces
234
234
  - [Products](products.md) — Product catalog and publication
235
- - [Order Routing](fulfillments.md#order-routing) — Channels can override the store's routing strategy
235
+ - [Order Routing](../how-to/custom-order-routing.md) — Channels can override the store's routing strategy
236
236
  - [Store SDK: Products](../sdk/store/products.md) — Channel-scoped product listing and filtering
237
237
  - [Admin SDK: Resources](../sdk/admin/resources.md) — How `adminClient.channels.addProducts` and other resource methods are structured
238
238
  - [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated channel driving the Next.js storefront's B2B surface
@@ -212,7 +212,8 @@ await adminClient.companies.taxIdentifiers.create('comp_xxx', {
212
212
  value: 'NL123456789B01',
213
213
  })
214
214
 
215
- // Validation runs against the official registry, in the background
215
+ // Out of the box this checks the number's format and checksum only.
216
+ // Registry lookups (such as VIES) come from an extension that re-registers the validator.
216
217
  await adminClient.companies.taxIdentifiers.validate('comp_xxx', 'txid_xxx')
217
218
  ```
218
219
 
@@ -72,7 +72,7 @@ const profiles = await adminClient.deliveryProfiles.list()
72
72
 
73
73
  // Ship a product as a download
74
74
  await adminClient.products.update('prod_xxx', {
75
- delivery_profile_id: 'dp_digital',
75
+ delivery_profile_id: 'fp_digital',
76
76
  })
77
77
  ```
78
78
 
@@ -85,7 +85,7 @@ curl 'https://api.mystore.com/api/v3/admin/delivery_profiles' \
85
85
  curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx' \
86
86
  -H 'X-Spree-API-Key: sk_xxx' \
87
87
  -H 'Content-Type: application/json' \
88
- -d '{ "delivery_profile_id": "dp_digital" }'
88
+ -d '{ "delivery_profile_id": "fp_digital" }'
89
89
  ```
90
90
 
91
91
 
@@ -60,7 +60,7 @@ Note the difference between `value` and `amount`. `value` is the rule — "10 pe
60
60
 
61
61
  ## Reading discounts on an order
62
62
 
63
- Whatever produced them, the rows read the same way:
63
+ The Store API summarizes the promotions applied to a cart or order under `discounts` — one entry per promotion, with its name, code and total amount. The individual discount rows, including manual ones, are read through the Admin API (`GET /api/v3/admin/orders/{id}/discounts`).
64
64
 
65
65
 
66
66
  ```typescript Store SDK
@@ -69,10 +69,10 @@ const cart = await client.carts.get(cartId)
69
69
  cart.display_discount_total // "-$12.00"
70
70
 
71
71
  cart.discounts.forEach((discount) => {
72
- discount.label // "Summer Sale"
73
- discount.kind // "promotion" or "manual"
72
+ discount.name // "Summer Sale"
74
73
  discount.code // "SUMMER20", when a code was used
75
74
  discount.display_amount // "-$12.00"
75
+ discount.promotion_id // "promo_xxx"
76
76
  })
77
77
  ```
78
78
 
@@ -44,12 +44,14 @@ On top of that, meaningful business moments get their own events:
44
44
  | `order.placed` | A customer completed checkout |
45
45
  | `order.paid` | Payment is settled in full |
46
46
  | `order.canceled` | The order was cancelled |
47
- | `order.shipped` / `order.delivered` | Everything has shipped / arrived |
47
+ | `order.fulfilled` / `order.delivered` | Everything has shipped / arrived |
48
48
  | `payment.completed` / `payment.voided` | A payment succeeded / was released |
49
- | `fulfillment.shipped` / `fulfillment.canceled` | A parcel went out / was stood down |
49
+ | `fulfillment.fulfilled` / `fulfillment.canceled` | A parcel went out / was stood down |
50
50
  | `product.out_of_stock` / `product.back_in_stock` | Availability flipped |
51
51
  | `return.received` / `return.refunded` | A return arrived / was refunded |
52
- | `import.completed` / `export.completed` | Bulk work finished |
52
+ | `import.completed` | An import finished |
53
+
54
+ For the full list of events and their payloads, see [Webhook Events & Payloads](../../api-reference/webhooks-events.md).
53
55
 
54
56
  The distinction matters when choosing what to listen for. `order.updated` fires whenever anything about an order changes — including an admin editing a note. `order.placed` fires once, when a customer actually bought something. Sending a confirmation email on the wrong one is how customers receive nine copies.
55
57
 
@@ -61,7 +63,6 @@ An event carries the record it's about, serialized:
61
63
  {
62
64
  "id": "or_86Rf07xd4z",
63
65
  "number": "R123456789",
64
- "status": "placed",
65
66
  "payment_status": "paid",
66
67
  "total": "135.60",
67
68
  "email": "customer@example.com"
@@ -128,7 +129,7 @@ That's almost always what you want. If you genuinely need to run inside the same
128
129
  subscribes_to 'order.placed', async: false
129
130
  ```
130
131
 
131
- > **WARNING:** A synchronous subscriber runs while the customer waits, and an exception in it can fail their checkout. Reserve it for work that must be atomic with the order, and keep it fast.
132
+ > **WARNING:** A synchronous subscriber runs while the customer waits, so a slow one slows their checkout. An exception in it is reported to `Rails.error` and swallowed in production, but re-raised in development and test so you notice it. Reserve it for work that must happen immediately, and keep it fast.
132
133
 
133
134
  ## Publishing your own events
134
135
 
@@ -14,7 +14,7 @@ Four pieces make that work, and none of them disturb retail shipping:
14
14
 
15
15
  | Piece | Question it answers |
16
16
  |---|---|
17
- | **Carton details** on a product | How do these units pack? |
17
+ | **Carton details** on a variant | How do these units pack? |
18
18
  | **Freight summary** on a cart or order | How big is the whole load? |
19
19
  | **Volume and company rules** on a delivery method | Which shipment tier is this? |
20
20
  | **Unpriced rates** | What does a buyer see before a price exists? |
@@ -35,7 +35,8 @@ weight**.
35
35
 
36
36
  The geometry lives on a shared carton, because merchants reuse a handful of
37
37
  standard sizes across hundreds of products and one edit should fix all of
38
- them. What varies per product stays on the product:
38
+ them. What varies per product is set on each variant (the product's only variant
39
+ for products without options):
39
40
 
40
41
  | Field | What it says |
41
42
  |---|---|
@@ -57,7 +57,7 @@ erDiagram
57
57
  | `delivery_rates` | The options the customer can choose from |
58
58
  | `selected_delivery_rate_id` | The chosen option |
59
59
  | `fulfilled_at` | When it went out |
60
- | `delivered_at` | When the customer got it — what return windows count from |
60
+ | `delivered_at` | When the customer got it |
61
61
 
62
62
  ## Statuses
63
63
 
@@ -104,9 +104,9 @@ only delivered when the third one lands. That rollup is recomputed whenever
104
104
  deliveries are added, removed or corrected — including when a tracking number
105
105
  is fixed, which starts that consignment's journey over.
106
106
 
107
- `delivered_at` is what return windows and the EU withdrawal period count from,
108
- so it records when the carrier says the parcel arrived rather than when you
109
- heard about it.
107
+ `delivered_at` records when the carrier says the parcel arrived rather than
108
+ when you heard about it. (The default return window counts from when the order
109
+ was placed, not from delivery — see [returns](returns-exchanges-claims.md).)
110
110
 
111
111
  Carriers report this through their provider's webhook, matched to the delivery
112
112
  by tracking number. With no carrier integration, staff mark receipt by hand —
@@ -216,11 +216,11 @@ Each fulfillment on a cart offers delivery rates. The customer picks one per ful
216
216
  const cart = await client.carts.get(cartId)
217
217
 
218
218
  cart.fulfillments[0].delivery_rates
219
- // => [{ id: 'rate_xxx', name: 'DHL Express', display_cost: '$12.00',
219
+ // => [{ id: 'dr_xxx', name: 'DHL Express', display_cost: '$12.00',
220
220
  // estimated_delivery_date: '2026-08-07' }, ...]
221
221
 
222
222
  await client.carts.fulfillments.update(cartId, cart.fulfillments[0].id, {
223
- selected_delivery_rate_id: 'rate_xxx',
223
+ selected_delivery_rate_id: 'dr_xxx',
224
224
  })
225
225
  ```
226
226
 
@@ -229,7 +229,7 @@ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx/fulfillments/
229
229
  -H 'X-Spree-API-Key: pk_xxx' \
230
230
  -H 'X-Spree-Token: abc123' \
231
231
  -H 'Content-Type: application/json' \
232
- -d '{ "selected_delivery_rate_id": "rate_xxx" }'
232
+ -d '{ "selected_delivery_rate_id": "dr_xxx" }'
233
233
  ```
234
234
 
235
235
 
@@ -286,7 +286,9 @@ await adminClient.orders.fulfillments.markDelivered('or_xxx', 'ful_xxx')
286
286
 
287
287
  // Split it when only part can go now
288
288
  await adminClient.orders.fulfillments.split('or_xxx', 'ful_xxx', {
289
- items: [{ fulfillment_item_id: 'fi_xxx', quantity: 1 }],
289
+ variant_id: 'variant_xxx',
290
+ quantity: 1,
291
+ stock_location_id: 'sloc_xxx', // optional; defaults to the fulfillment's location
290
292
  })
291
293
 
292
294
  // Cancel
@@ -45,8 +45,8 @@ const exportJob = await adminClient.exports.create({
45
45
 
46
46
  // Poll until it's done
47
47
  const status = await adminClient.exports.get(exportJob.id)
48
- status.status // "pending" → "processing" → "completed"
49
- status.download_url // available once completed
48
+ status.done // true once the file is ready
49
+ status.download_url // available once done
50
50
  ```
51
51
 
52
52
  ```bash CLI
@@ -63,7 +63,7 @@ An export can carry the same filters as the listing it came from, so "export wha
63
63
  ```typescript Admin SDK
64
64
  await adminClient.exports.create({
65
65
  type: 'orders',
66
- filters: { completed_at_gteq: '2026-01-01' },
66
+ search_params: { completed_at_gteq: '2026-01-01' },
67
67
  })
68
68
  ```
69
69
 
@@ -71,7 +71,7 @@ await adminClient.exports.create({
71
71
  curl -X POST 'https://api.mystore.com/api/v3/admin/exports' \
72
72
  -H 'X-Spree-API-Key: sk_xxx' \
73
73
  -H 'Content-Type: application/json' \
74
- -d '{ "type": "orders", "filters": { "completed_at_gteq": "2026-01-01" } }'
74
+ -d '{ "type": "orders", "search_params": { "completed_at_gteq": "2026-01-01" } }'
75
75
  ```
76
76
 
77
77
 
@@ -113,8 +113,8 @@ Importing has an extra step, because a file from somewhere else won't have Spree
113
113
 
114
114
  status.status // "processing" → "completed"
115
115
  status.rows_count // total
116
- status.processed_count
117
- status.failed_count
116
+ status.completed_rows_count
117
+ status.failed_rows_count
118
118
  ```
119
119
 
120
120
 
@@ -167,9 +167,12 @@ Imports and exports emit [events](events.md) as they progress, which also reach
167
167
 
168
168
  | Event | When |
169
169
  |---|---|
170
+ | `import.created` | An import has been uploaded |
171
+ | `import.progress` | A batch of rows has been processed |
170
172
  | `import.completed` | Every row has been attempted |
171
- | `import.failed` | The import could not run |
172
- | `export.completed` | The file is ready to download |
173
+ | `export.created` | An export has been requested (generation starts in the background) |
174
+
175
+ There is no event when an export's file is ready — poll the export until `done` is `true`.
173
176
 
174
177
  ## Permissions
175
178
 
@@ -240,7 +240,7 @@ await adminClient.stockTransfers.stockReceipts.create(transfer.id, {
240
240
  curl -X POST 'https://api.mystore.com/api/v3/admin/stock_transfers' \
241
241
  -H 'X-Spree-API-Key: sk_xxx' \
242
242
  -H 'Content-Type: application/json' \
243
- -d '{ "source_location_id": "sl_a", "destination_location_id": "sl_b" }'
243
+ -d '{ "source_location_id": "sloc_xxx", "destination_location_id": "sloc_yyy" }'
244
244
  ```
245
245
 
246
246
 
@@ -283,7 +283,7 @@ await adminClient.purchaseOrders.stockReceipts.create(order.id, {
283
283
  curl -X POST 'https://api.mystore.com/api/v3/admin/purchase_orders' \
284
284
  -H 'X-Spree-API-Key: sk_xxx' \
285
285
  -H 'Content-Type: application/json' \
286
- -d '{ "supplier_id": "sup_xxx", "stock_location_id": "sl_xxx" }'
286
+ -d '{ "supplier_id": "sup_xxx", "destination_location_id": "sloc_xxx" }'
287
287
  ```
288
288
 
289
289
 
@@ -28,7 +28,7 @@ Every file in a store is in the library, whether or not it's currently placed on
28
28
  const { data: media } = await adminClient.media.list()
29
29
 
30
30
  // Where is this file being used?
31
- const { data: usage } = await adminClient.media.usage('med_xxx')
31
+ const { data: usage } = await adminClient.media.usage('media_xxx')
32
32
  ```
33
33
 
34
34
  ```bash cURL
@@ -37,7 +37,7 @@ curl 'https://api.mystore.com/api/v3/admin/media' \
37
37
  -H 'X-Spree-API-Key: sk_xxx'
38
38
 
39
39
  # Where is this file being used?
40
- curl 'https://api.mystore.com/api/v3/admin/media/med_xxx/usage' \
40
+ curl 'https://api.mystore.com/api/v3/admin/media/media_xxx/usage' \
41
41
  -H 'X-Spree-API-Key: sk_xxx'
42
42
  ```
43
43
 
@@ -58,7 +58,7 @@ Placing a library file on a product creates a **new media row that shares the sa
58
58
 
59
59
  ```typescript Admin SDK
60
60
  await adminClient.products.media.create('prod_xxx', {
61
- source_media_id: 'med_xxx',
61
+ source_media_id: 'media_xxx',
62
62
  alt: 'Worn over a navy jumper',
63
63
  position: 2,
64
64
  })
@@ -68,7 +68,7 @@ await adminClient.products.media.create('prod_xxx', {
68
68
  curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_xxx/media' \
69
69
  -H 'X-Spree-API-Key: sk_xxx' \
70
70
  -H 'Content-Type: application/json' \
71
- -d '{ "source_media_id": "med_xxx", "alt": "Worn over a navy jumper", "position": 2 }'
71
+ -d '{ "source_media_id": "media_xxx", "alt": "Worn over a navy jumper", "position": 2 }'
72
72
  ```
73
73
 
74
74
 
@@ -83,19 +83,19 @@ A file in use can't be deleted by accident:
83
83
 
84
84
  ```typescript Admin SDK
85
85
  // Returns 422 with the list of places it's used
86
- await adminClient.media.delete('med_xxx')
86
+ await adminClient.media.delete('media_xxx')
87
87
 
88
88
  // Remove it from everywhere, then delete
89
- await adminClient.media.delete('med_xxx', { detach: true })
89
+ await adminClient.media.delete('media_xxx', { detach: true })
90
90
  ```
91
91
 
92
92
  ```bash cURL
93
93
  # Returns 422 with the list of places it is used
94
- curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/med_xxx' \
94
+ curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/media_xxx' \
95
95
  -H 'X-Spree-API-Key: sk_xxx'
96
96
 
97
97
  # Remove it from everywhere, then delete
98
- curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/med_xxx?detach=true' \
98
+ curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/media_xxx?detach=true' \
99
99
  -H 'X-Spree-API-Key: sk_xxx'
100
100
  ```
101
101
 
@@ -271,8 +271,8 @@ const video = await client.products.media.create('prod_86Rf07xd4z', {
271
271
  ```
272
272
 
273
273
  ```bash cURL
274
- curl -X POST 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media' \
275
- -H 'X-Spree-API-Key: pk_xxx' \
274
+ curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/media' \
275
+ -H 'X-Spree-API-Key: sk_xxx' \
276
276
  -H 'Content-Type: application/json' \
277
277
  -d '{
278
278
  "media_type": "video",
@@ -293,8 +293,8 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
293
293
  ```
294
294
 
295
295
  ```bash cURL
296
- curl -X PATCH 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
297
- -H 'X-Spree-API-Key: pk_xxx' \
296
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
297
+ -H 'X-Spree-API-Key: sk_xxx' \
298
298
  -H 'Content-Type: application/json' \
299
299
  -d '{ "poster_signed_id": "SIGNED_POSTER_BLOB_ID" }'
300
300
  ```
@@ -327,8 +327,8 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
327
327
  ```
328
328
 
329
329
  ```bash cURL
330
- curl -X PATCH 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
331
- -H 'X-Spree-API-Key: pk_xxx' \
330
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
331
+ -H 'X-Spree-API-Key: sk_xxx' \
332
332
  -H 'Content-Type: application/json' \
333
333
  -d '{ "focal_point_x": 0.25, "focal_point_y": 0.4 }'
334
334
  ```
@@ -141,11 +141,11 @@ await adminClient.orders.cancel('or_grouped_xxx', {
141
141
  })
142
142
 
143
143
  // Take an authorized payment
144
- await adminClient.orders.payments.capture('or_xxx', 'pay_xxx')
144
+ await adminClient.orders.payments.capture('or_xxx', 'py_xxx')
145
145
 
146
146
  // Refund against a specific payment
147
147
  await adminClient.orders.refunds.create('or_xxx', {
148
- payment_id: 'pay_xxx',
148
+ payment_id: 'py_xxx',
149
149
  amount: '25.00',
150
150
  })
151
151
  ```
@@ -109,7 +109,6 @@ erDiagram
109
109
  - **Payment Setup Session** manages saving payment methods for future use without an immediate charge (e.g., Stripe SetupIntent)
110
110
  - **Source** is polymorphic - can be a Credit Card, Payment Source (for alternative methods like Klarna, iDEAL), or Store Credit
111
111
  - **Gateway Customer** stores the provider-specific customer profile (e.g., Stripe Customer ID)
112
- - **Log Entries** record gateway responses for debugging
113
112
  - **Refunds** track money returned to customers
114
113
 
115
114
  ## Payment Methods
@@ -597,7 +596,7 @@ cannot start one from your storefront:
597
596
  await adminClient.orders.refunds.create(orderId, {
598
597
  payment_id: 'py_abc123',
599
598
  amount: '25.00',
600
- reason_id: 'rr_xyz789',
599
+ refund_reason_id: 'rr_xyz789',
601
600
  })
602
601
  ```
603
602
 
@@ -49,12 +49,12 @@ erDiagram
49
49
 
50
50
  OptionType {
51
51
  string name
52
- string presentation
52
+ string label
53
53
  }
54
54
 
55
55
  OptionValue {
56
56
  string name
57
- string presentation
57
+ string label
58
58
  }
59
59
  ```
60
60
 
@@ -607,9 +607,9 @@ erDiagram
607
607
  ```typescript Admin SDK
608
608
  const shoes = await adminClient.productTypes.create({
609
609
  name: 'Footwear',
610
- option_type_ids: ['optt_size', 'optt_color'],
610
+ option_type_ids: ['opt_size', 'opt_color'],
611
611
  category_ids: ['ctg_footwear'],
612
- delivery_profile_id: 'dp_standard',
612
+ delivery_profile_id: 'fp_standard',
613
613
  custom_field_definitions: [
614
614
  { id: 'cfdef_material', required: true, sort_order: 0 },
615
615
  ],
@@ -628,9 +628,9 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/product_types' \
628
628
  -H 'Content-Type: application/json' \
629
629
  -d '{
630
630
  "name": "Footwear",
631
- "option_type_ids": ["optt_size", "optt_color"],
631
+ "option_type_ids": ["opt_size", "opt_color"],
632
632
  "category_ids": ["ctg_footwear"],
633
- "delivery_profile_id": "dp_standard"
633
+ "delivery_profile_id": "fp_standard"
634
634
  }'
635
635
  ```
636
636
 
@@ -62,16 +62,17 @@ not a partial result computed from the half the server understood.
62
62
 
63
63
  ## Metrics
64
64
 
65
- A metric is an aggregate over one **base** relation. Core registers four, in
66
- three families:
65
+ A metric is an aggregate over one **base** relation. Core registers five, in
66
+ four families:
67
67
 
68
68
  | Family | Bases | Anchored on |
69
69
  |--------|-------|-------------|
70
70
  | `sales` | `:orders`, `:line_items` | when the order completed |
71
71
  | `payments` | `:payments` | when the payment was taken |
72
72
  | `inventory` | `:stock_movements` | when the stock moved |
73
+ | `carts` | `:carts` | when the cart was started |
73
74
 
74
- A query draws from **one family only**. The three answer different questions on
75
+ A query draws from **one family only**. The families answer different questions on
75
76
  different clocks, so a payment total beside a units-received count is two
76
77
  reports wearing one table — the query refuses the mix rather than inventing a
77
78
  join between grains that have no honest relationship.
@@ -56,7 +56,7 @@ Customers can open a return or a claim on their own order and follow its progres
56
56
  // Request a return — items refer to units that shipped, not cart line items
57
57
  const returnRequest = await client.orders.returns.create('or_xxx', {
58
58
  items: [{ fulfillment_item_id: 'fi_xxx', quantity: 1 }],
59
- reason_id: 'rsn_xxx',
59
+ reason_id: 'rar_xxx',
60
60
  memo: 'Too small',
61
61
  })
62
62
 
@@ -80,7 +80,7 @@ curl -X POST 'https://api.mystore.com/api/v3/store/orders/or_xxx/returns' \
80
80
  ```
81
81
 
82
82
 
83
- Claim types are `damaged`, `missing`, `wrong_item` and `other` out of the box, and a store can add its own.
83
+ Claim reasons are records each store owns. New stores are seeded with *Arrived damaged*, *Never arrived*, *Wrong item sent*, *Missing item from order* and *Item not as described*, and merchants can add, rename or remove their own.
84
84
 
85
85
  ## Processing a return
86
86
 
@@ -165,9 +165,9 @@ What to do about a claim is decided when you resolve it, not when the customer o
165
165
 
166
166
 
167
167
  ```typescript Admin SDK
168
- await adminClient.orders.claims.approve('or_xxx', 'clm_xxx')
168
+ await adminClient.orders.claims.approve('or_xxx', 'claim_xxx')
169
169
 
170
- await adminClient.orders.claims.resolve('or_xxx', 'clm_xxx', {
170
+ await adminClient.orders.claims.resolve('or_xxx', 'claim_xxx', {
171
171
  resolution: 'refund_and_replacement', // or 'refund', 'replacement'
172
172
  refund_method: 'store_credit',
173
173
  replacement_line_item_ids: ['li_xxx'],
@@ -175,10 +175,10 @@ await adminClient.orders.claims.resolve('or_xxx', 'clm_xxx', {
175
175
  ```
176
176
 
177
177
  ```bash cURL
178
- curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/approve' \
178
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/claim_xxx/approve' \
179
179
  -H 'X-Spree-API-Key: sk_xxx'
180
180
 
181
- curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/resolve' \
181
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/claim_xxx/resolve' \
182
182
  -H 'X-Spree-API-Key: sk_xxx' \
183
183
  -H 'Content-Type: application/json' \
184
184
  -d '{
@@ -240,7 +240,7 @@ curl 'https://api.mystore.com/api/v3/admin/claims?q[created_at_gt]=2026-08-01' \
240
240
 
241
241
  ## Return policy
242
242
 
243
- Spree ships no built-in return window. Whether a customer may open a return is store policy, and it's checked when the return is created — so you can hold customers to a 30-day window while letting staff make an exception for a good customer, and vary the rule by market where local law requires it.
243
+ Spree ships one built-in rule: a return window, counted from when the order was placed and set per market (30 days by default). Whether a customer may open a return is store policy, and it's checked when the return is created — so you can hold customers to that window while letting staff make an exception for a good customer, and vary the rule by market where local law requires it.
244
244
 
245
245
  Set the return window per market, or express a more specific rule in your own application. See [Configuration](../customization/configuration.md) and [Services & Workflows](../customization/workflows.md).
246
246
 
@@ -180,7 +180,7 @@ const filters = await client.products.filters()
180
180
  // filters: [
181
181
  // { id: 'price', type: 'price_range', min: 9.99, max: 199.99, currency: 'USD' },
182
182
  // { id: 'availability', type: 'availability', options: [{ id: 'in_stock', count: 42 }, { id: 'out_of_stock', count: 3 }] },
183
- // { id: 'optt_xxx', type: 'option', name: 'size', label: 'Size', kind: 'dropdown', options: [{ id: 'optval_xxx', name: 'Small', label: 'Small', position: 1, color_code: null, image_url: null, count: 12 }] },
183
+ // { id: 'opt_xxx', type: 'option', name: 'size', label: 'Size', kind: 'dropdown', options: [{ id: 'optval_xxx', name: 'Small', label: 'Small', position: 1, color_code: null, image_url: null, count: 12 }] },
184
184
  // { id: 'categories', type: 'category', options: [{ id: 'ctg_xxx', name: 'Clothing', permalink: 'clothing', count: 45 }] },
185
185
  // ],
186
186
  // sort_options: [{ id: 'manual' }, { id: 'price' }, { id: '-price' }],
@@ -276,10 +276,10 @@ See [Querying](../../api-reference/store-api/querying.md) for the full list of f
276
276
 
277
277
  ## Search Providers
278
278
 
279
- Spree uses a pluggable search provider architecture. The default provider uses SQL (ILIKE + Ransack). For production catalogs with 1,000+ products, we recommend switching to [Meilisearch](../../integrations/search/meilisearch.md) for typo tolerance, relevance ranking, and faster faceted search.
279
+ Spree uses a pluggable search provider architecture. The default provider uses SQL (ILIKE + Ransack). For production catalogs with 1,000+ products, we recommend installing the `spree_meilisearch` gem and switching to [Meilisearch](../../integrations/search/meilisearch.md) for typo tolerance, relevance ranking, and faster faceted search.
280
280
 
281
281
  ```ruby server/config/initializers/spree.rb
282
- Spree.search_provider = 'Spree::SearchProvider::Meilisearch'
282
+ Spree.search_provider = 'SpreeMeilisearch::SearchProvider'
283
283
  ```
284
284
 
285
285
  No client-side or API changes are needed — the same `q[search]`, filter params, and sort options work with any provider.