@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.
- package/dist/api-reference/store.yaml +78 -78
- package/dist/developer/agentic/agent-skills.md +1 -1
- package/dist/developer/cli/quickstart.md +2 -2
- package/dist/developer/core-concepts/addresses.md +13 -0
- package/dist/developer/core-concepts/calculators.md +6 -1
- package/dist/developer/core-concepts/carts.md +19 -1
- package/dist/developer/core-concepts/catalogs.md +51 -2
- package/dist/developer/core-concepts/channels.md +20 -1
- package/dist/developer/core-concepts/commissions.md +70 -1
- package/dist/developer/core-concepts/companies.md +74 -0
- package/dist/developer/core-concepts/customers.md +17 -5
- package/dist/developer/core-concepts/data-privacy.md +20 -7
- package/dist/developer/core-concepts/delivery-setup.md +57 -0
- package/dist/developer/core-concepts/discounts.md +7 -0
- package/dist/developer/core-concepts/events.md +13 -7
- package/dist/developer/core-concepts/fees.md +20 -0
- package/dist/developer/core-concepts/fulfillments.md +3 -3
- package/dist/developer/core-concepts/imports-exports.md +21 -5
- package/dist/developer/core-concepts/inventory.md +73 -1
- package/dist/developer/core-concepts/markets.md +5 -6
- package/dist/developer/core-concepts/media.md +66 -1
- package/dist/developer/core-concepts/metafields.md +1 -1
- package/dist/developer/core-concepts/order-totals.md +18 -1
- package/dist/developer/core-concepts/orders.md +2 -1
- package/dist/developer/core-concepts/payments.md +100 -20
- package/dist/developer/core-concepts/pricing.md +13 -8
- package/dist/developer/core-concepts/products.md +52 -1
- package/dist/developer/core-concepts/reporting.md +5 -5
- package/dist/developer/core-concepts/returns-exchanges-claims.md +41 -2
- package/dist/developer/core-concepts/search-filtering.md +1 -2
- package/dist/developer/core-concepts/sellers.md +109 -2
- package/dist/developer/core-concepts/slugs.md +1 -1
- package/dist/developer/core-concepts/staff-roles.md +23 -3
- package/dist/developer/core-concepts/stores.md +21 -0
- package/dist/developer/core-concepts/taxes.md +43 -0
- package/dist/developer/core-concepts/translations.md +9 -0
- package/dist/developer/core-concepts/webhooks.md +45 -3
- package/dist/developer/create-spree-app/quickstart.md +10 -10
- package/dist/developer/dashboard/deployment.md +4 -4
- package/dist/developer/dashboard/recipes/attribute-end-to-end.md +1 -1
- package/dist/developer/deployment/docker.md +2 -2
- package/dist/developer/how-to/custom-promotion.md +33 -13
- package/dist/developer/multi-tenant/quickstart.md +6 -6
- package/dist/developer/storefront/nextjs/testing.md +2 -2
- 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: '
|
|
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
|
-
|
|
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
|
|
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
|
-
├──
|
|
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 `
|
|
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 `
|
|
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 `
|
|
96
|
-
- **Override models** with decorators in `
|
|
97
|
-
- **Add controllers** in `
|
|
98
|
-
- **Configure Spree** in `
|
|
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 `
|
|
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**: `
|
|
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 (`
|
|
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
|
|
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 `
|
|
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: ./
|
|
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 `
|
|
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 `
|
|
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
|
|
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.
|