@spree/docs 0.1.229 → 0.1.231

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 (45) hide show
  1. package/dist/api-reference/store.yaml +78 -78
  2. package/dist/developer/agentic/agent-skills.md +1 -1
  3. package/dist/developer/cli/quickstart.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +13 -0
  5. package/dist/developer/core-concepts/calculators.md +6 -1
  6. package/dist/developer/core-concepts/carts.md +19 -1
  7. package/dist/developer/core-concepts/catalogs.md +51 -2
  8. package/dist/developer/core-concepts/channels.md +20 -1
  9. package/dist/developer/core-concepts/commissions.md +70 -1
  10. package/dist/developer/core-concepts/companies.md +74 -0
  11. package/dist/developer/core-concepts/customers.md +17 -5
  12. package/dist/developer/core-concepts/data-privacy.md +20 -7
  13. package/dist/developer/core-concepts/delivery-setup.md +57 -0
  14. package/dist/developer/core-concepts/discounts.md +7 -0
  15. package/dist/developer/core-concepts/events.md +13 -7
  16. package/dist/developer/core-concepts/fees.md +20 -0
  17. package/dist/developer/core-concepts/fulfillments.md +3 -3
  18. package/dist/developer/core-concepts/imports-exports.md +21 -5
  19. package/dist/developer/core-concepts/inventory.md +73 -1
  20. package/dist/developer/core-concepts/markets.md +5 -6
  21. package/dist/developer/core-concepts/media.md +66 -1
  22. package/dist/developer/core-concepts/metafields.md +1 -1
  23. package/dist/developer/core-concepts/order-totals.md +18 -1
  24. package/dist/developer/core-concepts/orders.md +2 -1
  25. package/dist/developer/core-concepts/payments.md +100 -20
  26. package/dist/developer/core-concepts/pricing.md +13 -8
  27. package/dist/developer/core-concepts/products.md +52 -1
  28. package/dist/developer/core-concepts/reporting.md +5 -5
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +41 -2
  30. package/dist/developer/core-concepts/search-filtering.md +1 -2
  31. package/dist/developer/core-concepts/sellers.md +109 -2
  32. package/dist/developer/core-concepts/slugs.md +1 -1
  33. package/dist/developer/core-concepts/staff-roles.md +23 -3
  34. package/dist/developer/core-concepts/stores.md +21 -0
  35. package/dist/developer/core-concepts/taxes.md +43 -0
  36. package/dist/developer/core-concepts/translations.md +9 -0
  37. package/dist/developer/core-concepts/webhooks.md +45 -3
  38. package/dist/developer/create-spree-app/quickstart.md +10 -10
  39. package/dist/developer/dashboard/deployment.md +4 -4
  40. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +1 -1
  41. package/dist/developer/deployment/docker.md +2 -2
  42. package/dist/developer/how-to/custom-promotion.md +33 -13
  43. package/dist/developer/multi-tenant/quickstart.md +6 -6
  44. package/dist/developer/storefront/nextjs/testing.md +2 -2
  45. package/package.json +1 -1
@@ -52,12 +52,62 @@ stateDiagram-v2
52
52
 
53
53
  Only an **approved** seller can sell, and even then not while they're on holiday — sellers can pause their own listings without the marketplace suspending them.
54
54
 
55
+ ## What a seller owns
56
+
57
+ A seller is a tenant inside the marketplace: their own catalogue, their own
58
+ stock, their own shipping setup and their own money trail. Delivery profiles and
59
+ zones stay with the operator — a seller picks from them rather than defining
60
+ them.
61
+
62
+ ```mermaid
63
+ erDiagram
64
+ Store ||--o{ Seller : "hosts"
65
+ Seller ||--o{ Product : "lists"
66
+ Seller ||--o{ StockLocation : "ships from"
67
+ Seller ||--o{ DeliveryMethod : "own rates"
68
+ Seller ||--o{ PackageType : "own packaging"
69
+ Seller ||--o{ Order : "fulfils"
70
+ Seller ||--o{ CommissionLine : "charged"
71
+ Seller ||--o{ SellerTransfer : "earns"
72
+ Seller ||--o{ SellerPayout : "paid out"
73
+ Seller ||--o{ SellerRequirementSubmission : "onboarding"
74
+ Seller ||--o{ Policy : "publishes"
75
+
76
+ Seller {
77
+ string name
78
+ string slug
79
+ string status
80
+ string contact_email
81
+ }
82
+ ```
83
+
84
+ A seller's delivery methods are their own — internal rates and manual
85
+ fulfillment, since carrier accounts stay with the operator. Their package types
86
+ sit alongside the marketplace's shared ones, so they can pack into the
87
+ operator's standard cartons or record their own.
88
+
89
+
55
90
  ```typescript Admin SDK
56
91
  await adminClient.sellers.invite('sel_xxx')
57
92
  await adminClient.sellers.approve('sel_xxx')
58
93
  await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery complaints' })
59
94
  ```
60
95
 
96
+ ```bash cURL
97
+ # Invite, approve, suspend
98
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
99
+ -H 'X-Spree-API-Key: sk_xxx'
100
+
101
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
102
+ -H 'X-Spree-API-Key: sk_xxx'
103
+
104
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/suspend' \
105
+ -H 'X-Spree-API-Key: sk_xxx' \
106
+ -H 'Content-Type: application/json' \
107
+ -d '{ "reason": "Unresolved delivery complaints" }'
108
+ ```
109
+
110
+
61
111
  Status is never set by writing to the field — each move is its own action, so approving a seller can run the checks that belong to approving.
62
112
 
63
113
  ## Onboarding requirements
@@ -66,17 +116,35 @@ What a vendor must do before selling differs by marketplace. A hardware marketpl
66
116
 
67
117
  So the checklist is **configured, not hardcoded**. Each store defines its own:
68
118
 
119
+
69
120
  ```typescript Admin SDK
70
121
  const { data: types } = await adminClient.sellerRequirements.types()
71
122
 
72
123
  await adminClient.sellerRequirements.create({
73
- type: 'Spree::SellerRequirements::MinimumProducts',
124
+ type: 'minimum_products',
74
125
  name: 'List at least three products',
75
126
  required: true,
76
127
  preferences: { minimum_count: 3 },
77
128
  })
78
129
  ```
79
130
 
131
+ ```bash cURL
132
+ # Requirement kinds this marketplace offers
133
+ curl 'https://api.mystore.com/api/v3/admin/seller_requirements/types' \
134
+ -H 'X-Spree-API-Key: sk_xxx'
135
+
136
+ # Add one to the checklist
137
+ curl -X POST 'https://api.mystore.com/api/v3/admin/seller_requirements' \
138
+ -H 'X-Spree-API-Key: sk_xxx' \
139
+ -H 'Content-Type: application/json' \
140
+ -d '{
141
+ "type": "minimum_products",
142
+ "name": "List at least three products",
143
+ "required": true
144
+ }'
145
+ ```
146
+
147
+
80
148
  Requirements come in three flavours, which is what lets one mechanism cover very different demands:
81
149
 
82
150
  | Kind | How it's satisfied |
@@ -89,6 +157,7 @@ Shipped out of the box: accepting terms, completing the profile, a billing addre
89
157
 
90
158
  A seller sees their checklist and its progress:
91
159
 
160
+
92
161
  ```typescript Seller SDK
93
162
  const onboarding = await sellerClient.onboarding.get()
94
163
 
@@ -102,6 +171,13 @@ onboarding.requirements.forEach((r) => {
102
171
  await sellerClient.onboarding.submitForReview()
103
172
  ```
104
173
 
174
+ ```bash cURL
175
+ curl 'https://marketplace.example.com/api/v3/seller/onboarding' \
176
+ -H 'Authorization: Bearer $SELLER_JWT' \
177
+ -H 'X-Spree-Seller-Id: sel_xxx'
178
+ ```
179
+
180
+
105
181
  The checklist is **worked out when read**, never stored on the seller. So a requirement added next month applies immediately to everyone, and nothing has to be backfilled.
106
182
 
107
183
  > **NOTE:** The checklist is enforced at exactly two moments — submitting for review, and approval. Approval refuses while a required item is outstanding, unless an operator deliberately overrides it.
@@ -114,11 +190,26 @@ A product can name a seller. No seller means it's the marketplace's own stock
114
190
 
115
191
  Sellers don't publish; they **submit**, and the marketplace decides. That review flow, and the statuses behind it, are covered in [Products](products.md#seller-submissions).
116
192
 
193
+
117
194
  ```typescript Seller SDK
118
195
  const product = await sellerClient.products.create({ name: 'Handmade Vase' })
119
196
  await sellerClient.products.submit(product.id)
120
197
  ```
121
198
 
199
+ ```bash cURL
200
+ # Create a product, then submit it for review
201
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/products' \
202
+ -H 'Authorization: Bearer $SELLER_JWT' \
203
+ -H 'X-Spree-Seller-Id: sel_xxx' \
204
+ -H 'Content-Type: application/json' \
205
+ -d '{ "name": "Handmade Vase" }'
206
+
207
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/products/prod_xxx/submit' \
208
+ -H 'Authorization: Bearer $SELLER_JWT' \
209
+ -H 'X-Spree-Seller-Id: sel_xxx'
210
+ ```
211
+
212
+
122
213
  Stores that trust their vendors can turn auto-approval on and skip the queue.
123
214
 
124
215
  ## One checkout, several sellers
@@ -156,12 +247,19 @@ Two details worth knowing:
156
247
 
157
248
  Commission is what the marketplace charges for the sale — configured as **rates**, and recorded per sale as immutable **commission lines**.
158
249
 
250
+
159
251
  ```typescript Admin SDK
160
252
  const { data: lines } = await adminClient.commissionLines.list({
161
- filter: { seller_id_eq: 'sel_xxx' },
253
+ seller_id_eq: 'sel_xxx',
162
254
  })
163
255
  ```
164
256
 
257
+ ```bash cURL
258
+ curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_xxx' \
259
+ -H 'X-Spree-API-Key: sk_xxx'
260
+ ```
261
+
262
+
165
263
  Three things are worth knowing here; the rest is on its own page.
166
264
 
167
265
  - **Rates are tried in list order**, and the first whose rules match wins. A rate with no rules matches everything, so the marketplace default belongs at the bottom.
@@ -184,6 +282,7 @@ Sellers get their own application — not access to the marketplace's dashboard.
184
282
 
185
283
  That last point is the security property worth relying on: a seller cannot ask for another seller's data, because there's nowhere in the request to name one.
186
284
 
285
+
187
286
  ```typescript Seller SDK
188
287
  import { createSellerClient } from '@spree/seller-sdk'
189
288
 
@@ -197,6 +296,14 @@ await sellerClient.orders.fulfillments.fulfill(orderId, fulfillmentId, {
197
296
  })
198
297
  ```
199
298
 
299
+ ```bash cURL
300
+ # Sign in, then use the returned JWT on every seller call
301
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/auth/login' \
302
+ -H 'Content-Type: application/json' \
303
+ -d '{ "email": "vendor@example.com", "password": "…" }'
304
+ ```
305
+
306
+
200
307
  Sellers can manage their profile, invite their own team, list and submit products, see and fulfil their orders, manage their stock locations, delivery methods and packaging, and work through onboarding. They cannot see other sellers, the marketplace's own catalog, or anything belonging to the store at large.
201
308
 
202
309
  What a seller's staff may do is governed by [roles](staff-roles.md) owned by the seller — the same permission system as the back office, with a narrower set of keys, so a seller role can never reach store settings.
@@ -59,7 +59,7 @@ If a slug already exists, Spree appends the SKU or a unique identifier to ensure
59
59
 
60
60
  Category slugs include the full parent path, making them hierarchical:
61
61
 
62
- ```
62
+ ```text
63
63
  clothing → "clothing"
64
64
  clothing/shirts → "clothing/shirts"
65
65
  clothing/shirts/t-shirts → "clothing/shirts/t-shirts"
@@ -44,6 +44,7 @@ Because roles are scoped to their owner, two stores can each define a "Manager"
44
44
 
45
45
  A permission key is a verb and a resource — `read_orders`, `write_products`. Roles hold a list of them:
46
46
 
47
+
47
48
  ```typescript Admin SDK
48
49
  const role = await adminClient.roles.create({
49
50
  name: 'Fulfillment staff',
@@ -52,12 +53,31 @@ const role = await adminClient.roles.create({
52
53
  })
53
54
  ```
54
55
 
56
+ ```bash cURL
57
+ curl -X POST 'https://api.mystore.com/api/v3/admin/roles' \
58
+ -H 'X-Spree-API-Key: sk_xxx' \
59
+ -H 'Content-Type: application/json' \
60
+ -d '{
61
+ "name": "Fulfillment staff",
62
+ "description": "Can see orders and ship them, nothing else",
63
+ "permissions": ["read_orders", "write_fulfillments", "read_products"]
64
+ }'
65
+ ```
66
+
67
+
55
68
  Every grantable key is discoverable, so a permission picker never needs a hardcoded list:
56
69
 
70
+
57
71
  ```typescript Admin SDK
58
72
  const { data: permissions } = await adminClient.permissions.list()
59
73
  ```
60
74
 
75
+ ```bash cURL
76
+ curl 'https://api.mystore.com/api/v3/admin/permissions' \
77
+ -H 'X-Spree-API-Key: sk_xxx'
78
+ ```
79
+
80
+
61
81
  Keys are grouped so they can be presented sensibly — orders, catalog, marketing, customers, settings, access and analytics.
62
82
 
63
83
  | Group | Covers |
@@ -84,13 +104,13 @@ A role is deletable only when nothing depends on it: staff assignments and pendi
84
104
 
85
105
  Use the Spree CLI to create admin users:
86
106
 
87
- ```bash
107
+ ```bash Spree CLI
88
108
  spree user create
89
109
  ```
90
110
 
91
111
  The CLI will prompt you for the email and password. You can also pass them directly:
92
112
 
93
- ```bash
113
+ ```bash Spree CLI
94
114
  spree user create --email admin@example.com --password secret123
95
115
  ```
96
116
 
@@ -140,7 +160,7 @@ You can invite new admins through the Admin Panel or programmatically.
140
160
 
141
161
  Using the [Admin SDK](../sdk/admin/quickstart.md), call `client.invitations.create`:
142
162
 
143
- ```typescript
163
+ ```typescript Admin SDK
144
164
  import { createAdminClient } from '@spree/admin-sdk'
145
165
 
146
166
  const client = createAdminClient({
@@ -7,6 +7,27 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
7
7
 
8
8
  The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
9
9
 
10
+ ```mermaid
11
+ erDiagram
12
+ Store ||--o{ Channel : "sells through"
13
+ Store ||--o{ Market : "trades in"
14
+ Store ||--o{ Product : "catalogs"
15
+ Store ||--o{ Order : "records"
16
+ Store ||--o{ PaymentMethod : "accepts"
17
+ Store ||--o{ StockLocation : "ships from"
18
+ Store ||--o{ Policy : "publishes"
19
+ Store ||--o{ Seller : "hosts"
20
+
21
+ Store {
22
+ string name
23
+ string code
24
+ string url
25
+ string default_currency
26
+ string default_locale
27
+ boolean default
28
+ }
29
+ ```
30
+
10
31
  ## Store Attributes
11
32
 
12
33
  | Attribute | Description |
@@ -36,6 +36,7 @@ Every product carries one. If you don't choose, the default is used.
36
36
  | `is_default` | Used by products that don't pick one | `true` |
37
37
  | `tax_code` | The code your tax service knows it by | `1257L` |
38
38
 
39
+
39
40
  ```typescript Admin SDK
40
41
  const reduced = await adminClient.taxCategories.create({
41
42
  name: 'Reduced rate',
@@ -43,6 +44,14 @@ const reduced = await adminClient.taxCategories.create({
43
44
  })
44
45
  ```
45
46
 
47
+ ```bash cURL
48
+ curl -X POST 'https://api.mystore.com/api/v3/admin/tax_categories' \
49
+ -H 'X-Spree-API-Key: sk_xxx' \
50
+ -H 'Content-Type: application/json' \
51
+ -d '{ "name": "Reduced rate", "tax_code": "1257L" }'
52
+ ```
53
+
54
+
46
55
  `tax_code` matters once you connect an external service — it is how you tell that service what kind of thing this is, in the vocabulary it already uses.
47
56
 
48
57
  ## Tax rates
@@ -57,6 +66,7 @@ A tax rate is the number itself, plus where it applies and whether it's already
57
66
  | `country_code` / `state_code` | Where it applies | `DE`, or `US` + `CA` |
58
67
  | `tax_category` | What it applies to | `Standard` |
59
68
 
69
+
60
70
  ```typescript Admin SDK
61
71
  const { data: rates } = await adminClient.taxRates.list()
62
72
 
@@ -69,6 +79,25 @@ await adminClient.taxRates.create({
69
79
  })
70
80
  ```
71
81
 
82
+ ```bash cURL
83
+ # List tax rates
84
+ curl 'https://api.mystore.com/api/v3/admin/tax_rates' \
85
+ -H 'X-Spree-API-Key: sk_xxx'
86
+
87
+ # Create one
88
+ curl -X POST 'https://api.mystore.com/api/v3/admin/tax_rates' \
89
+ -H 'X-Spree-API-Key: sk_xxx' \
90
+ -H 'Content-Type: application/json' \
91
+ -d '{
92
+ "name": "VAT 20%",
93
+ "amount": "0.2",
94
+ "included_in_price": true,
95
+ "country_code": "GB",
96
+ "tax_category_id": "taxcat_xxx"
97
+ }'
98
+ ```
99
+
100
+
72
101
  Rates name their country and state by **ISO code**, not by an ID — `DE`, `US` + `CA`. There's no separate region record to create first.
73
102
 
74
103
  ## Included or added on
@@ -120,10 +149,17 @@ Whatever works out the tax, the record is the same: a tax line per charge, sayin
120
149
  | `amount` / `display_amount` | The tax charged |
121
150
  | `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
122
151
 
152
+
123
153
  ```typescript Admin SDK
124
154
  const { data: taxLines } = await adminClient.orders.taxLines.list('or_xxx')
125
155
  ```
126
156
 
157
+ ```bash cURL
158
+ curl 'https://api.mystore.com/api/v3/admin/orders/or_xxx/tax_lines' \
159
+ -H 'X-Spree-API-Key: sk_xxx'
160
+ ```
161
+
162
+
127
163
  Tax lines attach to a line item, a [fulfillment](fulfillments.md), or a [fee](fees.md) — so tax on goods and tax on delivery stay separately visible, which is what a tax return needs.
128
164
 
129
165
  Each line also keeps its own copy of the rate and label. If someone edits that tax rate next year, the order still says what the customer was actually charged.
@@ -153,10 +189,17 @@ Spree's built-in rate tables are fine when the rules are simple — one country,
153
189
 
154
190
  For those, connect a real tax service. **The provider is chosen per [Market](markets.md)**, which means one store can use its own rates in a simple market and an external service in a complicated one.
155
191
 
192
+
156
193
  ```typescript Admin SDK
157
194
  const { data: providers } = await adminClient.taxProviders.list()
158
195
  ```
159
196
 
197
+ ```bash cURL
198
+ curl 'https://api.mystore.com/api/v3/admin/tax_providers' \
199
+ -H 'X-Spree-API-Key: sk_xxx'
200
+ ```
201
+
202
+
160
203
  A provider declares up front what it cannot do — no US local tax, no reverse charge — so the dashboard can warn a merchant who pairs it with a market that needs it, rather than quietly under-collecting.
161
204
 
162
205
  Whichever provider is in use, the result is the same: tax lines on the order, in the same shape. Your storefront code doesn't change.
@@ -87,6 +87,7 @@ Merchants translate in the dashboard, switching locale on the record they're edi
87
87
 
88
88
  Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
89
89
 
90
+
90
91
  ```typescript Admin SDK
91
92
  await adminClient.products.update('prod_xxx', {
92
93
  translations: {
@@ -99,6 +100,14 @@ await adminClient.products.update('prod_xxx', {
99
100
  const { data: resources } = await adminClient.translatableResources.list()
100
101
  ```
101
102
 
103
+ ```bash cURL
104
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx' \
105
+ -H 'X-Spree-API-Key: sk_xxx' \
106
+ -H 'Content-Type: application/json' \
107
+ -d '{ "translations": { "de": { "name": "Baumwolltasche" } } }'
108
+ ```
109
+
110
+
102
111
  Asking the API which resources are translatable means a translation tool doesn't need a hardcoded list that goes stale.
103
112
 
104
113
  ## Translating the interface
@@ -83,6 +83,17 @@ spree api post /webhook_endpoints -d '{
83
83
  }'
84
84
  ```
85
85
 
86
+ ```bash cURL
87
+ curl -X POST 'https://api.mystore.com/api/v3/admin/webhook_endpoints' \
88
+ -H 'X-Spree-API-Key: sk_xxx' \
89
+ -H 'Content-Type: application/json' \
90
+ -d '{
91
+ "url": "https://example.com/webhooks/spree",
92
+ "subscriptions": ["order.*", "product.created"],
93
+ "active": true
94
+ }'
95
+ ```
96
+
86
97
 
87
98
  > **WARNING:** The `secret_key` is returned **only once**, in the create response. Save it immediately — it can't be retrieved later and is required to verify webhook signatures.
88
99
 
@@ -113,6 +124,13 @@ spree api patch /webhook_endpoints/whe_xxx \
113
124
  -d '{"subscriptions": ["order.placed", "order.canceled"]}'
114
125
  ```
115
126
 
127
+ ```bash cURL
128
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/webhook_endpoints/whe_xxx' \
129
+ -H 'X-Spree-API-Key: sk_xxx' \
130
+ -H 'Content-Type: application/json' \
131
+ -d '{ "subscriptions": ["order.placed", "order.canceled"] }'
132
+ ```
133
+
116
134
 
117
135
  The `subscriptions` array accepts exact event names and wildcard patterns:
118
136
 
@@ -128,7 +146,7 @@ The `subscriptions` array accepts exact event names and wildcard patterns:
128
146
 
129
147
  Each webhook delivery sends a JSON payload with the following structure. The `data` object uses the same [Store API V3 serializers](../../api-reference/introduction.md) as the REST API, so webhook payloads and API responses share the same schema:
130
148
 
131
- ```json
149
+ ```json Payload
132
150
  {
133
151
  "id": "550e8400-e29b-41d4-a716-446655440000",
134
152
  "name": "order.placed",
@@ -188,7 +206,7 @@ The [Spree Storefront](https://github.com/spree/storefront) includes a ready-mad
188
206
 
189
207
  Use `@spree/sdk/webhooks` for framework-agnostic verification:
190
208
 
191
- ```typescript
209
+ ```typescript Store SDK
192
210
  import { verifyWebhookSignature } from '@spree/sdk/webhooks'
193
211
  import type { WebhookEvent } from '@spree/sdk/webhooks'
194
212
  import type { Order } from '@spree/sdk'
@@ -211,7 +229,7 @@ app.post('/webhooks/spree', async (req, res) => {
211
229
 
212
230
  #### Ruby
213
231
 
214
- ```ruby
232
+ ```ruby server/app/controllers/webhooks_controller.rb
215
233
  class WebhooksController < ApplicationController
216
234
  skip_before_action :verify_authenticity_token
217
235
 
@@ -272,6 +290,14 @@ spree api get /webhook_endpoints/whe_xxx/deliveries -q event_name_eq=order.place
272
290
  spree api post /webhook_endpoints/whe_xxx/deliveries/whd_xxx/redeliver
273
291
  ```
274
292
 
293
+ ```bash cURL
294
+ curl 'https://api.mystore.com/api/v3/admin/webhook_endpoints/whe_xxx/deliveries?q[event_name_eq]=order.placed' \
295
+ -H 'X-Spree-API-Key: sk_xxx'
296
+
297
+ curl -X POST 'https://api.mystore.com/api/v3/admin/webhook_endpoints/whe_xxx/deliveries/whd_xxx/redeliver' \
298
+ -H 'X-Spree-API-Key: sk_xxx'
299
+ ```
300
+
275
301
 
276
302
  ### Delivery Attributes
277
303
 
@@ -339,6 +365,17 @@ spree api post /webhook_endpoints -d '{
339
365
  spree api post /webhook_endpoints/whe_xxx/send_test
340
366
  ```
341
367
 
368
+ ```bash cURL
369
+ curl -X POST 'https://api.mystore.com/api/v3/admin/webhook_endpoints' \
370
+ -H 'X-Spree-API-Key: sk_xxx' \
371
+ -H 'Content-Type: application/json' \
372
+ -d '{
373
+ "url": "https://your-ngrok-url.ngrok.io/webhooks",
374
+ "subscriptions": ["order.*"],
375
+ "active": true
376
+ }'
377
+ ```
378
+
342
379
 
343
380
  `send_test` delivers a synthetic `webhook.test` event so you can verify the endpoint is reachable and your signature-verification code works, without having to trigger a real order.
344
381
 
@@ -386,6 +423,11 @@ const failed = deliveries[0]
386
423
  spree api get /webhook_endpoints/whe_xxx/deliveries -q success_eq=false
387
424
  ```
388
425
 
426
+ ```bash cURL
427
+ curl 'https://api.mystore.com/api/v3/admin/webhook_endpoints/whe_xxx/deliveries?q[success_eq]=false' \
428
+ -H 'X-Spree-API-Key: sk_xxx'
429
+ ```
430
+
389
431
 
390
432
  ## Related Documentation
391
433
 
@@ -53,14 +53,14 @@ npx create-spree-app@latest my-store --no-storefront --no-sample-data --no-start
53
53
  ```text
54
54
  my-store/
55
55
  ├── docker-compose.yml # Spree backend (prebuilt image) + Postgres + Meilisearch
56
- ├── docker-compose.dev.yml # Alternative: build from local backend/
56
+ ├── docker-compose.dev.yml # Alternative: build from local server/
57
57
  ├── render.yaml # Render Blueprint — one Docker service built from this repo
58
58
  ├── .env # SECRET_KEY_BASE, SPREE_PORT, SPREE_VERSION_TAG, SPREE_SAMPLE_DATA
59
59
  ├── .dockerignore # Keeps the production build context to sources
60
60
  ├── .gitignore
61
61
  ├── package.json # Convenience scripts
62
62
  ├── README.md
63
- ├── backend/ # Full Rails app — the Spree API (from spree/spree-starter)
63
+ ├── server/ # Full Rails app — the Spree API (from spree/spree-starter)
64
64
  │ ├── Gemfile
65
65
  │ ├── Dockerfile # Also builds the production image (API + dashboard)
66
66
  │ ├── config/
@@ -84,18 +84,18 @@ my-store/
84
84
 
85
85
  ## Customizing the Spree API
86
86
 
87
- The `backend/` directory is the Spree API — a full Rails application with Spree installed (cloned from [spree-starter](https://github.com/spree/spree-starter)) serving the Store and Admin APIs your storefront and dashboard talk to, plus background jobs and transactional emails. By default, the project runs it from a prebuilt Docker image. To switch to building from your local copy:
87
+ The `server/` directory is the Spree API — a full Rails application with Spree installed (cloned from [spree-starter](https://github.com/spree/spree-starter)) serving the Store and Admin APIs your storefront and dashboard talk to, plus background jobs and transactional emails. By default, the project runs it from a prebuilt Docker image. To switch to building from your local copy:
88
88
 
89
89
  ```bash
90
90
  npx spree eject
91
91
  ```
92
92
 
93
- This replaces `docker-compose.yml` with a version that builds from `backend/`, rebuilds the image, and restarts services. You can then:
93
+ This replaces `docker-compose.yml` with a version that builds from `server/`, rebuilds the image, and restarts services. You can then:
94
94
 
95
- - **Add gems** to `backend/Gemfile`
96
- - **Override models** with decorators in `backend/app/models/`
97
- - **Add controllers** in `backend/app/controllers/`
98
- - **Configure Spree** in `backend/config/initializers/spree.rb`
95
+ - **Add gems** to `server/Gemfile`
96
+ - **Override models** with decorators in `server/app/models/`
97
+ - **Add controllers** in `server/app/controllers/`
98
+ - **Configure Spree** in `server/config/initializers/spree.rb`
99
99
  - **Add migrations** with `spree generate migration AddFooToSpreeBars foo:string` (runs inside the container)
100
100
 
101
101
  See the [Customization Guide](../customization.md) for more details.
@@ -109,7 +109,7 @@ The project includes [@spree/cli](../cli/quickstart.md) for managing your Spree
109
109
  | `spree dev` | Run the app in the foreground — streams logs, Ctrl+C stops it. First run completes setup automatically; co-runs the React Dashboard dev server when `apps/dashboard` exists |
110
110
  | `spree stop` | Stop backend services |
111
111
  | `spree update` | Pull latest Spree image and restart (runs migrations automatically) |
112
- | `spree eject` | Switch from prebuilt image to building from `backend/` |
112
+ | `spree eject` | Switch from prebuilt image to building from `server/` |
113
113
  | `spree add dashboard` | Add the React Dashboard to an existing project |
114
114
  | `spree build --production` | Build the production image — the Spree API plus your dashboard, in one |
115
115
  | `spree logs` | View backend logs |
@@ -162,7 +162,7 @@ SPREE_VERSION_TAG=5.4
162
162
 
163
163
  ## Deployment
164
164
 
165
- The project deploys as **one image**: `backend/Dockerfile` builds the Spree API together with your React Dashboard (when `apps/dashboard` exists), served same-origin at `/dashboard` — no CORS, no cookie configuration, no second service.
165
+ The project deploys as **one image**: `server/Dockerfile` builds the Spree API together with your React Dashboard (when `apps/dashboard` exists), served same-origin at `/dashboard` — no CORS, no cookie configuration, no second service.
166
166
 
167
167
  - **Render** — the `render.yaml` at the project root is a ready Blueprint: one Docker service built straight from your repo, migrations run on boot.
168
168
  - **Anywhere else** — `spree build --production` builds the same image locally; push it to a registry and run it on any Docker host.
@@ -22,13 +22,13 @@ The official image contains the *stock* dashboard. Once you've customized yours
22
22
  spree build --production # optionally --tag registry/repo:tag
23
23
  ```
24
24
 
25
- The Dockerfile normalizes its own build context: built from the project root it detects the layout (`backend/` = the Rails app, `apps/dashboard/` = your dashboard) and builds *your* dashboard into the image — no flags, no named contexts. Without the CLI, the equivalent is:
25
+ The Dockerfile normalizes its own build context: built from the project root it detects the layout (`server/` = the Rails app, `apps/dashboard/` = your dashboard) and builds *your* dashboard into the image — no flags, no named contexts. Without the CLI, the equivalent is:
26
26
 
27
27
  ```bash
28
- docker build . -f backend/Dockerfile -t my-shop-spree:latest
28
+ docker build . -f server/Dockerfile -t my-shop-spree:latest
29
29
  ```
30
30
 
31
- Inside the dashboard stage the build runs with `VITE_BASE_PATH=/dashboard/` (asset URLs resolve under the mount) and `VITE_SPREE_API_URL` unset (API calls stay origin-relative); the Node toolchain lives only in that throw-away stage. Projects **without** `apps/dashboard/` get the stock dashboard baked instead — and if your `backend/Dockerfile` predates this layout support, the command warns instead of silently shipping the stock dashboard over your customized one (update it from the spree-starter template).
31
+ Inside the dashboard stage the build runs with `VITE_BASE_PATH=/dashboard/` (asset URLs resolve under the mount) and `VITE_SPREE_API_URL` unset (API calls stay origin-relative); the Node toolchain lives only in that throw-away stage. Projects **without** `apps/dashboard/` get the stock dashboard baked instead — and if your `server/Dockerfile` predates this layout support, the command warns instead of silently shipping the stock dashboard over your customized one (update it from the spree-starter template).
32
32
 
33
33
  ## Static host / CDN — the alternative
34
34
 
@@ -53,7 +53,7 @@ The Blueprint that `create-spree-app` places at the project root deploys the bac
53
53
 
54
54
  ```yaml
55
55
  runtime: docker
56
- dockerfilePath: ./backend/Dockerfile
56
+ dockerfilePath: ./server/Dockerfile
57
57
  dockerContext: .
58
58
  ```
59
59
 
@@ -28,7 +28,7 @@ Lead time is a column because its whole point is the query: *"show me everything
28
28
 
29
29
  ## 1. Backend
30
30
 
31
- Four small pieces — each one unlocks a specific frontend capability. In a host app these live in `backend/` (or `api/`); in a distributed plugin, in the Rails engine half.
31
+ Four small pieces — each one unlocks a specific frontend capability. In a host app these live in `server/` (or `api/`); in a distributed plugin, in the Rails engine half.
32
32
 
33
33
  **The column** (with an index — you're adding it *because* it will be queried):
34
34
 
@@ -13,7 +13,7 @@ If you haven't ejected yet, do that first:
13
13
  spree eject
14
14
  ```
15
15
 
16
- This generates the app source — including its production `Dockerfile` — in `backend/`. Make your customizations, then build:
16
+ This generates the app source — including its production `Dockerfile` — in `server/`. Make your customizations, then build:
17
17
 
18
18
  ```bash
19
19
  spree build --production # → <project>-spree:latest
@@ -23,7 +23,7 @@ spree build --production --tag registry.example.com/my-store:v42
23
23
  Or with plain Docker, from the project root:
24
24
 
25
25
  ```bash
26
- docker build . -f backend/Dockerfile -t my-store
26
+ docker build . -f server/Dockerfile -t my-store
27
27
  ```
28
28
 
29
29
  Both produce the same image — and it's exactly what Render and Railway build from your repository. Migrations run automatically on boot.