@spree/docs 0.1.247 → 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.
- package/dist/api-reference/admin-api/authentication.md +34 -14
- package/dist/api-reference/admin-api/endpoints.md +366 -14
- package/dist/api-reference/admin-api/errors.md +2 -2
- package/dist/api-reference/admin-api/introduction.md +3 -3
- package/dist/api-reference/admin-api/querying.md +6 -6
- package/dist/api-reference/store-api/monetary-amounts.md +5 -5
- package/dist/api-reference/webhooks-events.md +330 -335
- package/dist/developer/agentic/agent-skills.md +5 -2
- package/dist/developer/agentic/llm-docs.md +2 -1
- package/dist/developer/cli/admin-api.md +1 -1
- package/dist/developer/cli/quickstart.md +2 -2
- package/dist/developer/contributing/creating-an-extension.md +292 -146
- package/dist/developer/contributing/developing-spree.md +13 -17
- package/dist/developer/core-concepts/catalogs.md +2 -2
- package/dist/developer/core-concepts/channels.md +3 -3
- package/dist/developer/core-concepts/companies.md +2 -1
- package/dist/developer/core-concepts/delivery-setup.md +2 -2
- package/dist/developer/core-concepts/discounts.md +3 -3
- package/dist/developer/core-concepts/events.md +6 -5
- package/dist/developer/core-concepts/freight.md +3 -2
- package/dist/developer/core-concepts/fulfillments.md +10 -8
- package/dist/developer/core-concepts/imports-exports.md +11 -8
- package/dist/developer/core-concepts/inventory.md +2 -2
- package/dist/developer/core-concepts/media.md +14 -14
- package/dist/developer/core-concepts/orders.md +2 -2
- package/dist/developer/core-concepts/payments.md +1 -2
- package/dist/developer/core-concepts/products.md +6 -6
- package/dist/developer/core-concepts/reporting.md +4 -3
- package/dist/developer/core-concepts/returns-exchanges-claims.md +7 -7
- package/dist/developer/core-concepts/search-filtering.md +3 -3
- package/dist/developer/core-concepts/sellers.md +22 -3
- package/dist/developer/core-concepts/staff-roles.md +4 -2
- package/dist/developer/core-concepts/store-credits-gift-cards.md +1 -1
- package/dist/developer/core-concepts/stores.md +2 -2
- package/dist/developer/core-concepts/translations.md +12 -8
- package/dist/developer/core-concepts/webhooks.md +19 -18
- package/dist/developer/create-spree-app/quickstart.md +2 -7
- package/dist/developer/customization/api.md +1 -1
- package/dist/developer/customization/checkout.md +2 -2
- package/dist/developer/customization/dependencies.md +53 -37
- package/dist/developer/customization/permissions.md +2 -2
- package/dist/developer/dashboard/concepts.md +1 -1
- package/dist/developer/dashboard/customization/navigation.md +3 -2
- package/dist/developer/dashboard/customization/permissions.md +6 -6
- package/dist/developer/dashboard/plugins/publishing.md +4 -4
- package/dist/developer/dashboard/plugins/scaffolding.md +1 -1
- package/dist/developer/dashboard/public-api.md +1 -1
- package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -18
- package/dist/developer/deployment/aws.md +1 -1
- package/dist/developer/deployment/aws_ecs.md +3 -3
- package/dist/developer/deployment/background_jobs.md +9 -3
- package/dist/developer/deployment/docker.md +1 -2
- package/dist/developer/deployment/emails.md +3 -1
- package/dist/developer/deployment/environment_variables.md +2 -2
- package/dist/developer/deployment/render.md +2 -2
- package/dist/developer/how-to/build-a-marketplace.md +2 -2
- package/dist/developer/how-to/custom-api-authentication.md +1 -1
- package/dist/developer/how-to/custom-delivery-rate-provider.md +11 -3
- package/dist/developer/how-to/custom-document-numbers.md +1 -1
- package/dist/developer/how-to/custom-order-routing.md +15 -14
- package/dist/developer/how-to/custom-payment-method.md +17 -19
- package/dist/developer/how-to/custom-promotion.md +4 -4
- package/dist/developer/how-to/custom-search-provider.md +12 -5
- package/dist/developer/how-to/custom-stock-splitter.md +25 -24
- package/dist/developer/how-to/sell-digital-products.md +1 -1
- package/dist/developer/multi-tenant/quickstart.md +2 -2
- package/dist/developer/providers/payouts.md +6 -2
- package/dist/developer/sdk/admin/querying-and-errors.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +4 -4
- package/dist/developer/sdk/authentication.md +5 -2
- package/dist/developer/sdk/store/cart-checkout.md +4 -4
- package/dist/developer/storefront/nextjs/emails.md +4 -2
- package/dist/developer/storefront/nextjs/testing.md +1 -1
- package/dist/developer/upgrades/5.6-to-6.0.md +51 -20
- package/dist/integrations/search/meilisearch.md +4 -4
- 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,
|
|
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)
|
|
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,
|
|
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
|
-
| `
|
|
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`, `
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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": "
|
|
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](
|
|
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](
|
|
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](
|
|
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
|
-
//
|
|
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: '
|
|
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": "
|
|
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
|
-
|
|
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.
|
|
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.
|
|
47
|
+
| `order.fulfilled` / `order.delivered` | Everything has shipped / arrived |
|
|
48
48
|
| `payment.completed` / `payment.voided` | A payment succeeded / was released |
|
|
49
|
-
| `fulfillment.
|
|
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`
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
108
|
-
|
|
109
|
-
|
|
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: '
|
|
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: '
|
|
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": "
|
|
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
|
-
|
|
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.
|
|
49
|
-
status.download_url // available once
|
|
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
|
-
|
|
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", "
|
|
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.
|
|
117
|
-
status.
|
|
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
|
-
| `
|
|
172
|
-
|
|
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": "
|
|
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", "
|
|
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('
|
|
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/
|
|
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: '
|
|
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": "
|
|
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('
|
|
86
|
+
await adminClient.media.delete('media_xxx')
|
|
87
87
|
|
|
88
88
|
// Remove it from everywhere, then delete
|
|
89
|
-
await adminClient.media.delete('
|
|
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/
|
|
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/
|
|
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/
|
|
275
|
-
-H 'X-Spree-API-Key:
|
|
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/
|
|
297
|
-
-H 'X-Spree-API-Key:
|
|
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/
|
|
331
|
-
-H 'X-Spree-API-Key:
|
|
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', '
|
|
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: '
|
|
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
|
-
|
|
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
|
|
52
|
+
string label
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
OptionValue {
|
|
56
56
|
string name
|
|
57
|
-
string
|
|
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: ['
|
|
610
|
+
option_type_ids: ['opt_size', 'opt_color'],
|
|
611
611
|
category_ids: ['ctg_footwear'],
|
|
612
|
-
delivery_profile_id: '
|
|
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": ["
|
|
631
|
+
"option_type_ids": ["opt_size", "opt_color"],
|
|
632
632
|
"category_ids": ["ctg_footwear"],
|
|
633
|
-
"delivery_profile_id": "
|
|
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
|
|
66
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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', '
|
|
168
|
+
await adminClient.orders.claims.approve('or_xxx', 'claim_xxx')
|
|
169
169
|
|
|
170
|
-
await adminClient.orders.claims.resolve('or_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/
|
|
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/
|
|
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
|
|
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: '
|
|
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 = '
|
|
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.
|