@spree/docs 0.1.230 → 0.1.232
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 +28 -0
- 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/store-credits-gift-cards.md +60 -23
- 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
|
@@ -163,6 +163,7 @@ how postage records work in general.
|
|
|
163
163
|
|
|
164
164
|
What to do about a claim is decided when you resolve it, not when the customer opens it — merchants usually decide once they've seen the photos.
|
|
165
165
|
|
|
166
|
+
|
|
166
167
|
```typescript Admin SDK
|
|
167
168
|
await adminClient.orders.claims.approve('or_xxx', 'clm_xxx')
|
|
168
169
|
|
|
@@ -173,32 +174,70 @@ await adminClient.orders.claims.resolve('or_xxx', 'clm_xxx', {
|
|
|
173
174
|
})
|
|
174
175
|
```
|
|
175
176
|
|
|
177
|
+
```bash cURL
|
|
178
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/approve' \
|
|
179
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
180
|
+
|
|
181
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/resolve' \
|
|
182
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
183
|
+
-H 'Content-Type: application/json' \
|
|
184
|
+
-d '{
|
|
185
|
+
"resolution": "refund_and_replacement",
|
|
186
|
+
"refund_method": "store_credit",
|
|
187
|
+
"replacement_line_item_ids": ["li_xxx"]
|
|
188
|
+
}'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
|
|
176
192
|
A replacement creates a new [fulfillment](fulfillments.md) on the original order, so the customer doesn't have to place a second one.
|
|
177
193
|
|
|
178
194
|
## Exchanges
|
|
179
195
|
|
|
180
196
|
An exchange works like a return, but ends by sending different items instead of refunding:
|
|
181
197
|
|
|
198
|
+
|
|
182
199
|
```typescript Admin SDK
|
|
183
200
|
await adminClient.orders.exchanges.approve('or_xxx', 'exch_xxx')
|
|
184
201
|
await adminClient.orders.exchanges.receive('or_xxx', 'exch_xxx')
|
|
185
202
|
await adminClient.orders.exchanges.fulfill('or_xxx', 'exch_xxx')
|
|
186
203
|
```
|
|
187
204
|
|
|
205
|
+
```bash cURL
|
|
206
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/approve' \
|
|
207
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
208
|
+
|
|
209
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/receive' \
|
|
210
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
211
|
+
|
|
212
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/fulfill' \
|
|
213
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
|
|
188
217
|
## Reporting
|
|
189
218
|
|
|
190
219
|
Because each one is its own record, you can query them directly:
|
|
191
220
|
|
|
221
|
+
|
|
192
222
|
```typescript Admin SDK
|
|
193
223
|
// Everything awaiting action
|
|
194
|
-
const pending = await adminClient.returns.list({
|
|
224
|
+
const pending = await adminClient.returns.list({ status_eq: 'approved' })
|
|
195
225
|
|
|
196
226
|
// Claims opened this month
|
|
197
227
|
const claims = await adminClient.claims.list({
|
|
198
|
-
|
|
228
|
+
created_at_gt: '2026-08-01',
|
|
199
229
|
})
|
|
200
230
|
```
|
|
201
231
|
|
|
232
|
+
```bash cURL
|
|
233
|
+
curl 'https://api.mystore.com/api/v3/admin/returns?q[status_eq]=approved' \
|
|
234
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
235
|
+
|
|
236
|
+
curl 'https://api.mystore.com/api/v3/admin/claims?q[created_at_gt]=2026-08-01' \
|
|
237
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
|
|
202
241
|
## Return policy
|
|
203
242
|
|
|
204
243
|
Spree ships no built-in return window. Whether a customer may open a return is store policy, and it's checked when the return is created — so you can hold customers to a 30-day window while letting staff make an exception for a good customer, and vary the rule by market where local law requires it.
|
|
@@ -278,8 +278,7 @@ See [Querying](../../api-reference/store-api/querying.md) for the full list of f
|
|
|
278
278
|
|
|
279
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.
|
|
280
280
|
|
|
281
|
-
```ruby
|
|
282
|
-
# config/initializers/spree.rb
|
|
281
|
+
```ruby server/config/initializers/spree.rb
|
|
283
282
|
Spree.search_provider = 'Spree::SearchProvider::Meilisearch'
|
|
284
283
|
```
|
|
285
284
|
|
|
@@ -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({
|
|
@@ -74,26 +74,22 @@ erDiagram
|
|
|
74
74
|
| `currency` | Currency code | `USD` |
|
|
75
75
|
| `memo` | Optional note about the credit | `Refund for order #R123` |
|
|
76
76
|
|
|
77
|
-
###
|
|
77
|
+
### Why a Credit Exists
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Store credits carry no category and no type — both were retired in Spree 6.0
|
|
80
|
+
because neither answered a question anything asked. Two fields cover it
|
|
81
|
+
instead:
|
|
80
82
|
|
|
81
|
-
|
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| Non-expiring | Store credit that does not expire |
|
|
83
|
+
| Field | What it tells you |
|
|
84
|
+
|-------|-------------------|
|
|
85
|
+
| `originator` | The return, exchange, claim or gift card that issued the credit, or nothing when an admin issued it by hand |
|
|
86
|
+
| `memo` | The free-text reason, written by the refund workflows or by the admin |
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
Store credits do not expire; only gift cards carry an expiry, on their own
|
|
89
|
+
record. Which credit is spent first is decided by `Spree::StoreCredit.oldest_first`.
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Types define the priority in which store credits are applied at checkout:
|
|
92
|
-
|
|
93
|
-
| Type | Description |
|
|
94
|
-
|------|-------------|
|
|
95
|
-
| Expiring | Credits that may expire (applied first) |
|
|
96
|
-
| Non-expiring | Credits that don't expire (applied last) |
|
|
91
|
+
> **NOTE:** `Spree::StoreCreditCategory` and `Spree::StoreCreditType` survive as deprecated
|
|
92
|
+
> shells for one release and are removed in 6.1. Nothing writes them.
|
|
97
93
|
|
|
98
94
|
### Store Credit Events
|
|
99
95
|
|
|
@@ -115,7 +111,7 @@ Store credits are managed in the Admin Panel:
|
|
|
115
111
|
2. Select a customer
|
|
116
112
|
3. Go to the **Store Credits** tab
|
|
117
113
|
4. Click **Add Store Credit**
|
|
118
|
-
5. Enter the amount,
|
|
114
|
+
5. Enter the amount, currency, and optional memo
|
|
119
115
|
6. Click **Create**
|
|
120
116
|
|
|
121
117
|
Store credits can also be created via the [Admin API](../../api-reference/admin-api/introduction.md), as a nested resource under the customer:
|
|
@@ -132,7 +128,6 @@ const client = createAdminClient({
|
|
|
132
128
|
const credit = await client.customers.storeCredits.create('cus_xxx', {
|
|
133
129
|
amount: '25.00',
|
|
134
130
|
currency: 'USD',
|
|
135
|
-
category_id: 'sccat_xxx',
|
|
136
131
|
memo: 'Goodwill credit for delayed shipment',
|
|
137
132
|
})
|
|
138
133
|
```
|
|
@@ -141,12 +136,55 @@ const credit = await client.customers.storeCredits.create('cus_xxx', {
|
|
|
141
136
|
spree api post /customers/cus_xxx/store_credits -d '{
|
|
142
137
|
"amount": "25.00",
|
|
143
138
|
"currency": "USD",
|
|
144
|
-
"category_id": "sccat_xxx",
|
|
145
139
|
"memo": "Goodwill credit"
|
|
146
140
|
}'
|
|
147
141
|
```
|
|
148
142
|
|
|
149
143
|
|
|
144
|
+
### Listing Store Credits Across Customers
|
|
145
|
+
|
|
146
|
+
Credits are written per customer, but read across them. The Admin API exposes a
|
|
147
|
+
read-only list so an integration can answer what the store owes without walking
|
|
148
|
+
every customer:
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
```typescript Admin SDK
|
|
152
|
+
const { data: credits, meta } = await client.storeCredits.list({
|
|
153
|
+
outstanding: true,
|
|
154
|
+
expand: ['customer', 'created_by'],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// One row per currency, summed over the same filter as the page.
|
|
158
|
+
meta.totals.forEach((total) => {
|
|
159
|
+
console.log(total.currency, total.display_amount_remaining)
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```bash CLI
|
|
164
|
+
spree api get /store_credits -q 'outstanding=true'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
Filters mirror the questions a merchant asks: `customer_id`,
|
|
169
|
+
`customer_email_cont`, `currency`, `created_by_id`, `memo_cont`, a
|
|
170
|
+
`created_at` range, and two scopes — `outstanding` (money still owed versus
|
|
171
|
+
money spent) and `from_gift_card` (whether a gift card redemption created it).
|
|
172
|
+
|
|
173
|
+
Each credit's ledger is its own endpoint:
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
```typescript Admin SDK
|
|
177
|
+
const { data: events } = await client.storeCredits.events.list('credit_xxx')
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```bash CLI
|
|
181
|
+
spree api get /store_credits/credit_xxx/events
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
Creating, updating and deleting a credit remain nested under the customer that
|
|
186
|
+
holds it — a credit without an owner has nobody to pay.
|
|
187
|
+
|
|
150
188
|
### Store Credit Events
|
|
151
189
|
|
|
152
190
|
The store credit system publishes lifecycle events:
|
|
@@ -387,10 +425,9 @@ flowchart TB
|
|
|
387
425
|
|
|
388
426
|
### Store Credit Priority
|
|
389
427
|
|
|
390
|
-
When a registered customer
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
2. **Non-expiring credits** - Applied after expiring credits
|
|
428
|
+
When a registered customer holds several store credits, the oldest is spent
|
|
429
|
+
first (`Spree::StoreCredit.oldest_first`). Store credits do not expire — only
|
|
430
|
+
gift cards carry an expiry, on their own record.
|
|
394
431
|
|
|
395
432
|
## Related Documentation
|
|
396
433
|
|
|
@@ -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
|