@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
|
@@ -22,6 +22,7 @@ flowchart LR
|
|
|
22
22
|
|
|
23
23
|
Every file in a store is in the library, whether or not it's currently placed on anything. That means a file can be uploaded ahead of time, browsed, searched, and reused.
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
```typescript Admin SDK
|
|
26
27
|
// Browse everything in the store
|
|
27
28
|
const { data: media } = await adminClient.media.list()
|
|
@@ -30,6 +31,17 @@ const { data: media } = await adminClient.media.list()
|
|
|
30
31
|
const { data: usage } = await adminClient.media.usage('med_xxx')
|
|
31
32
|
```
|
|
32
33
|
|
|
34
|
+
```bash cURL
|
|
35
|
+
# Browse everything in the store
|
|
36
|
+
curl 'https://api.mystore.com/api/v3/admin/media' \
|
|
37
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
38
|
+
|
|
39
|
+
# Where is this file being used?
|
|
40
|
+
curl 'https://api.mystore.com/api/v3/admin/media/med_xxx/usage' \
|
|
41
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
|
|
33
45
|
| Field | What it tells you |
|
|
34
46
|
|---|---|
|
|
35
47
|
| `attached` | Whether the file is placed on anything yet |
|
|
@@ -43,6 +55,7 @@ const { data: usage } = await adminClient.media.usage('med_xxx')
|
|
|
43
55
|
|
|
44
56
|
Placing a library file on a product creates a **new media row that shares the same underlying file**:
|
|
45
57
|
|
|
58
|
+
|
|
46
59
|
```typescript Admin SDK
|
|
47
60
|
await adminClient.products.media.create('prod_xxx', {
|
|
48
61
|
source_media_id: 'med_xxx',
|
|
@@ -51,6 +64,14 @@ await adminClient.products.media.create('prod_xxx', {
|
|
|
51
64
|
})
|
|
52
65
|
```
|
|
53
66
|
|
|
67
|
+
```bash cURL
|
|
68
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_xxx/media' \
|
|
69
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
70
|
+
-H 'Content-Type: application/json' \
|
|
71
|
+
-d '{ "source_media_id": "med_xxx", "alt": "Worn over a navy jumper", "position": 2 }'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
The copy gets its own alt text, its own position, and its own variant links — because the same photograph might be the third image on one product and the hero on another, with different alt text describing what matters in each context. What it doesn't get is a second copy of the bytes.
|
|
55
76
|
|
|
56
77
|
> **NOTE:** This is why there's no single "shared asset" record to manage. Each placement is independently editable, while storage is shared. Detaching a photo from one product has no effect on any other.
|
|
@@ -59,6 +80,7 @@ The copy gets its own alt text, its own position, and its own variant links —
|
|
|
59
80
|
|
|
60
81
|
A file in use can't be deleted by accident:
|
|
61
82
|
|
|
83
|
+
|
|
62
84
|
```typescript Admin SDK
|
|
63
85
|
// Returns 422 with the list of places it's used
|
|
64
86
|
await adminClient.media.delete('med_xxx')
|
|
@@ -67,6 +89,17 @@ await adminClient.media.delete('med_xxx')
|
|
|
67
89
|
await adminClient.media.delete('med_xxx', { detach: true })
|
|
68
90
|
```
|
|
69
91
|
|
|
92
|
+
```bash cURL
|
|
93
|
+
# Returns 422 with the list of places it is used
|
|
94
|
+
curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/med_xxx' \
|
|
95
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
96
|
+
|
|
97
|
+
# Remove it from everywhere, then delete
|
|
98
|
+
curl -X DELETE 'https://api.mystore.com/api/v3/admin/media/med_xxx?detach=true' \
|
|
99
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
|
|
70
103
|
The dashboard shows you the usage list and asks before doing the second one. Check `usage` before offering a delete in your own tooling.
|
|
71
104
|
|
|
72
105
|
## What can carry media
|
|
@@ -227,6 +260,7 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/medi
|
|
|
227
260
|
|
|
228
261
|
A hosted video is uploaded the same way an image is, with `media_type` telling Spree what it is. Add `poster_signed_id` to give it a still frame:
|
|
229
262
|
|
|
263
|
+
|
|
230
264
|
```typescript Admin SDK
|
|
231
265
|
const video = await client.products.media.create('prod_86Rf07xd4z', {
|
|
232
266
|
media_type: 'video',
|
|
@@ -236,14 +270,36 @@ const video = await client.products.media.create('prod_86Rf07xd4z', {
|
|
|
236
270
|
})
|
|
237
271
|
```
|
|
238
272
|
|
|
273
|
+
```bash cURL
|
|
274
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media' \
|
|
275
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
276
|
+
-H 'Content-Type: application/json' \
|
|
277
|
+
-d '{
|
|
278
|
+
"media_type": "video",
|
|
279
|
+
"signed_id": "SIGNED_VIDEO_BLOB_ID",
|
|
280
|
+
"poster_signed_id": "SIGNED_POSTER_BLOB_ID",
|
|
281
|
+
"alt": "Product in use"
|
|
282
|
+
}'
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
|
|
239
286
|
A poster can also be added or replaced later, on its own:
|
|
240
287
|
|
|
288
|
+
|
|
241
289
|
```typescript Admin SDK
|
|
242
290
|
await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
|
|
243
291
|
poster_signed_id: signedPosterBlobId,
|
|
244
292
|
})
|
|
245
293
|
```
|
|
246
294
|
|
|
295
|
+
```bash cURL
|
|
296
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
|
|
297
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
298
|
+
-H 'Content-Type: application/json' \
|
|
299
|
+
-d '{ "poster_signed_id": "SIGNED_POSTER_BLOB_ID" }'
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
|
|
247
303
|
> **INFO:** Spree serves an uploaded video as you uploaded it — it does not transcode. Keep files web-friendly (H.264 MP4 or WebM) so they play everywhere, and prefer an external video for long footage so the provider handles streaming.
|
|
248
304
|
|
|
249
305
|
#### Posters
|
|
@@ -262,6 +318,7 @@ Spree does not extract a frame from an uploaded video, so give hosted video and
|
|
|
262
318
|
|
|
263
319
|
`focal_point_x` and `focal_point_y` mark the part of an image that must stay in frame when a storefront crops it to a different shape. Both are fractions between 0 and 1, measured from the top left, so `{ x: 0.5, y: 0.5 }` is dead centre — which is also what a storefront should assume when they are null.
|
|
264
320
|
|
|
321
|
+
|
|
265
322
|
```typescript Admin SDK
|
|
266
323
|
await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
|
|
267
324
|
focal_point_x: 0.25,
|
|
@@ -269,6 +326,14 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
|
|
|
269
326
|
})
|
|
270
327
|
```
|
|
271
328
|
|
|
329
|
+
```bash cURL
|
|
330
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z/media/media_k5nR8xLq' \
|
|
331
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
332
|
+
-H 'Content-Type: application/json' \
|
|
333
|
+
-d '{ "focal_point_x": 0.25, "focal_point_y": 0.4 }'
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
|
|
272
337
|
Spree stores the focal point and serves it; the cropping itself is the storefront's decision, since only it knows the shape it needs.
|
|
273
338
|
|
|
274
339
|
### Named Variant Sizes
|
|
@@ -349,7 +414,7 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,vari
|
|
|
349
414
|
|
|
350
415
|
**Response (media object):**
|
|
351
416
|
|
|
352
|
-
```json
|
|
417
|
+
```json Response
|
|
353
418
|
{
|
|
354
419
|
"id": "media_k5nR8xLq",
|
|
355
420
|
"media_type": "image",
|
|
@@ -170,7 +170,7 @@ A custom field can take part in product listings — but only if you ask, becaus
|
|
|
170
170
|
|
|
171
171
|
Setting either flag also makes the field **filterable**. Filter and sort with the field's `cf_` key, on both APIs:
|
|
172
172
|
|
|
173
|
-
```
|
|
173
|
+
```http
|
|
174
174
|
GET /api/v3/store/products?q[cf_properties_material_i_cont]=wool
|
|
175
175
|
GET /api/v3/store/products?sort=-cf_properties_weight
|
|
176
176
|
GET /api/v3/admin/products?q[cf_properties_weight_gteq]=3.5
|
|
@@ -22,6 +22,7 @@ flowchart TB
|
|
|
22
22
|
|
|
23
23
|
## The totals
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
```typescript Store SDK
|
|
26
27
|
const order = await client.orders.get('or_xxx')
|
|
27
28
|
|
|
@@ -33,6 +34,13 @@ order.display_tax_total // "$22.60" all tax
|
|
|
33
34
|
order.display_total // "$138.10" what the customer pays
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
```bash cURL
|
|
38
|
+
curl 'https://api.mystore.com/api/v3/store/orders/or_xxx' \
|
|
39
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
40
|
+
-H 'Authorization: Bearer $CUSTOMER_JWT'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
36
44
|
| Attribute | What it sums |
|
|
37
45
|
|---|---|
|
|
38
46
|
| `item_total` | Line item prices, before tax and discounts |
|
|
@@ -49,7 +57,7 @@ order.display_total // "$138.10" what the customer pays
|
|
|
49
57
|
|
|
50
58
|
Every total comes twice: `total` is the raw value, `display_total` is formatted for the order's currency.
|
|
51
59
|
|
|
52
|
-
```json
|
|
60
|
+
```json Response
|
|
53
61
|
{
|
|
54
62
|
"total": "138.10",
|
|
55
63
|
"display_total": "$138.10"
|
|
@@ -74,6 +82,7 @@ The two exist because the same order can carry both: VAT already inside the good
|
|
|
74
82
|
|
|
75
83
|
Every change to a cart returns the whole cart, totals included:
|
|
76
84
|
|
|
85
|
+
|
|
77
86
|
```typescript Store SDK
|
|
78
87
|
const cart = await client.carts.items.create(cartId, {
|
|
79
88
|
variant_id: 'var_xxx',
|
|
@@ -83,6 +92,14 @@ const cart = await client.carts.items.create(cartId, {
|
|
|
83
92
|
cart.display_total // already correct
|
|
84
93
|
```
|
|
85
94
|
|
|
95
|
+
```bash cURL
|
|
96
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/items' \
|
|
97
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
98
|
+
-H 'Content-Type: application/json' \
|
|
99
|
+
-d '{ "variant_id": "variant_xxx", "quantity": 2 }'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
|
|
86
103
|
There is never a second "recalculate" request to make, and never a moment where the summary on screen disagrees with what the server thinks. Adding an item, entering an address, applying a code — each response carries the new numbers.
|
|
87
104
|
|
|
88
105
|
The totals are worked out again at the moment the cart is completed, so a price that changed while the customer sat on the review page can't lead to the wrong charge.
|
|
@@ -114,7 +114,8 @@ Back-office work happens through the Admin API.
|
|
|
114
114
|
```typescript Admin SDK
|
|
115
115
|
// Find orders needing attention
|
|
116
116
|
const orders = await adminClient.orders.list({
|
|
117
|
-
|
|
117
|
+
payment_status_eq: 'paid',
|
|
118
|
+
fulfillment_status_eq: 'unfulfilled',
|
|
118
119
|
})
|
|
119
120
|
|
|
120
121
|
// Cancel an order, recording why from your own list of reasons
|
|
@@ -9,7 +9,7 @@ Spree has a highly flexible payments model which allows multiple payment methods
|
|
|
9
9
|
|
|
10
10
|
Payment methods typically represent a payment gateway. Gateways will process card payments, online bank transfers, buy-now-pay-later, wallet payments, and other types of payments. Spree also comes with a Check option for offline processing.
|
|
11
11
|
|
|
12
|
-
The `Payment` model in Spree tracks payments against [Orders](orders). Payments relate to a `source` which indicates how the payment was made, and a `PaymentMethod`, indicating the processor used for this payment.
|
|
12
|
+
The `Payment` model in Spree tracks payments against [Orders](orders.md). Payments relate to a `source` which indicates how the payment was made, and a `PaymentMethod`, indicating the processor used for this payment.
|
|
13
13
|
|
|
14
14
|
When a payment is created, it is given a unique, 8-character identifier. This is used when sending the payment details to the payment processor. Without this identifier, some payment gateways mistakenly reported duplicate payments.
|
|
15
15
|
|
|
@@ -153,7 +153,8 @@ Payment methods where `session_required?` returns `false` don't need a payment s
|
|
|
153
153
|
|
|
154
154
|
For these methods, the Store API allows creating a payment directly without going through the payment session flow:
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
|
|
157
|
+
```typescript Store SDK
|
|
157
158
|
const options = { spreeToken: cart.token }
|
|
158
159
|
|
|
159
160
|
// Create a payment for a non-session payment method
|
|
@@ -166,6 +167,19 @@ const payment = await client.carts.payments.create(cart.id, {
|
|
|
166
167
|
}, options)
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
```bash cURL
|
|
171
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payments' \
|
|
172
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
173
|
+
-H 'X-Spree-Token: CART_TOKEN' \
|
|
174
|
+
-H 'Content-Type: application/json' \
|
|
175
|
+
-d '{
|
|
176
|
+
"payment_method_id": "pm_xyz789",
|
|
177
|
+
"amount": "99.99",
|
|
178
|
+
"metadata": { "purchase_order_number": "PO-12345" }
|
|
179
|
+
}'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
|
|
169
183
|
The payment is created in `checkout` state. When the order transitions to `complete`, Spree calls `process_payments!` which runs the payment method's `authorize` (or `purchase` if auto-capture is enabled). For manual payment methods like Check, this is a no-op that succeeds immediately — the payment moves to `pending` (or `completed` with auto-capture), allowing the order to complete.
|
|
170
184
|
|
|
171
185
|
The merchant can later capture or void the payment from the Admin Panel once the actual payment is received (e.g., check arrives, bank transfer clears, cash is collected on delivery).
|
|
@@ -349,7 +363,8 @@ stateDiagram-v2
|
|
|
349
363
|
|
|
350
364
|
**Create a Payment Session:**
|
|
351
365
|
|
|
352
|
-
|
|
366
|
+
|
|
367
|
+
```typescript Store SDK
|
|
353
368
|
const options = { spreeToken: cart.token }
|
|
354
369
|
|
|
355
370
|
// Create a payment session for the selected payment method
|
|
@@ -364,9 +379,18 @@ console.log(session.external_id) // e.g. 'pi_3ABC123' (Stripe Payme
|
|
|
364
379
|
console.log(session.external_data.client_secret) // Use with Stripe.js or Adyen Drop-in
|
|
365
380
|
```
|
|
366
381
|
|
|
382
|
+
```bash cURL
|
|
383
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payment_sessions' \
|
|
384
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
385
|
+
-H 'X-Spree-Token: CART_TOKEN' \
|
|
386
|
+
-H 'Content-Type: application/json' \
|
|
387
|
+
-d '{ "payment_method_id": "pm_xyz789", "amount": "99.99", "external_data": {} }'
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
|
|
367
391
|
**Response shape (`StorePaymentSession`):**
|
|
368
392
|
|
|
369
|
-
```json
|
|
393
|
+
```json Response
|
|
370
394
|
{
|
|
371
395
|
"id": "ps_abc123",
|
|
372
396
|
"status": "pending",
|
|
@@ -386,7 +410,8 @@ console.log(session.external_data.client_secret) // Use with Stripe.js or Adyen
|
|
|
386
410
|
|
|
387
411
|
**Update a Payment Session** (e.g., after order total changes):
|
|
388
412
|
|
|
389
|
-
|
|
413
|
+
|
|
414
|
+
```typescript Store SDK
|
|
390
415
|
const updated = await client.carts.paymentSessions.update(
|
|
391
416
|
cart.id, session.id,
|
|
392
417
|
{ amount: '149.99' },
|
|
@@ -394,9 +419,19 @@ const updated = await client.carts.paymentSessions.update(
|
|
|
394
419
|
)
|
|
395
420
|
```
|
|
396
421
|
|
|
422
|
+
```bash cURL
|
|
423
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payment_sessions/ps_xxx' \
|
|
424
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
425
|
+
-H 'X-Spree-Token: CART_TOKEN' \
|
|
426
|
+
-H 'Content-Type: application/json' \
|
|
427
|
+
-d '{ "amount": "149.99" }'
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
|
|
397
431
|
**Complete a Payment Session** (after customer confirms payment on the frontend):
|
|
398
432
|
|
|
399
|
-
|
|
433
|
+
|
|
434
|
+
```typescript Store SDK
|
|
400
435
|
const completed = await client.carts.paymentSessions.complete(
|
|
401
436
|
cart.id, session.id,
|
|
402
437
|
{ session_result: '...', external_data: {} },
|
|
@@ -405,14 +440,31 @@ const completed = await client.carts.paymentSessions.complete(
|
|
|
405
440
|
console.log(completed.status) // 'completed'
|
|
406
441
|
```
|
|
407
442
|
|
|
443
|
+
```bash cURL
|
|
444
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payment_sessions/ps_xxx/complete' \
|
|
445
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
446
|
+
-H 'X-Spree-Token: CART_TOKEN' \
|
|
447
|
+
-H 'Content-Type: application/json' \
|
|
448
|
+
-d '{ "session_result": "...", "external_data": {} }'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
|
|
408
452
|
> **WARNING:** Completing a payment session does **not** complete the order. You must call `POST /carts/:id/complete` separately after the session is completed. This separation prevents race conditions between the frontend and payment webhooks.
|
|
409
453
|
|
|
410
454
|
**Complete the Order** (after the payment session is completed):
|
|
411
455
|
|
|
412
|
-
|
|
456
|
+
|
|
457
|
+
```typescript Store SDK
|
|
413
458
|
const order = await client.carts.complete(cart.id, options)
|
|
414
459
|
```
|
|
415
460
|
|
|
461
|
+
```bash cURL
|
|
462
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/complete' \
|
|
463
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
464
|
+
-H 'X-Spree-Token: CART_TOKEN'
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
|
|
416
468
|
### Payment Webhooks
|
|
417
469
|
|
|
418
470
|
Spree provides a generic webhook endpoint at `POST /api/v3/webhooks/payments/:payment_method_id` that payment gateway extensions can use. When a payment provider sends a webhook (e.g., Stripe `payment_intent.succeeded`), Spree:
|
|
@@ -427,7 +479,7 @@ The background job creates/updates the Payment record, marks the session as comp
|
|
|
427
479
|
|
|
428
480
|
Gateway extensions implement `parse_webhook_event` to normalize provider-specific payloads:
|
|
429
481
|
|
|
430
|
-
```ruby
|
|
482
|
+
```ruby server/app/models/my_gateway.rb
|
|
431
483
|
class MyGateway < Spree::Gateway
|
|
432
484
|
def parse_webhook_event(raw_body, headers)
|
|
433
485
|
# Verify signature — raise WebhookSignatureError if invalid
|
|
@@ -591,7 +643,8 @@ sequenceDiagram
|
|
|
591
643
|
|
|
592
644
|
**Create a Payment Setup Session:**
|
|
593
645
|
|
|
594
|
-
|
|
646
|
+
|
|
647
|
+
```typescript Store SDK
|
|
595
648
|
const options = { token: jwtToken }
|
|
596
649
|
|
|
597
650
|
// Create a setup session for saving a payment method
|
|
@@ -604,9 +657,18 @@ const setupSession = await client.customer.paymentSetupSessions.create({
|
|
|
604
657
|
console.log(setupSession.external_client_secret) // e.g. 'seti_ABC123_secret_xyz'
|
|
605
658
|
```
|
|
606
659
|
|
|
660
|
+
```bash cURL
|
|
661
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_sessions' \
|
|
662
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
663
|
+
-H 'Authorization: Bearer $CUSTOMER_JWT' \
|
|
664
|
+
-H 'Content-Type: application/json' \
|
|
665
|
+
-d '{ "payment_method_id": "pm_xyz789", "external_data": {} }'
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
|
|
607
669
|
**Response shape (`StorePaymentSetupSession`):**
|
|
608
670
|
|
|
609
|
-
```json
|
|
671
|
+
```json Response
|
|
610
672
|
{
|
|
611
673
|
"id": "pss_abc123",
|
|
612
674
|
"status": "pending",
|
|
@@ -622,13 +684,22 @@ console.log(setupSession.external_client_secret) // e.g. 'seti_ABC123_secret_xyz
|
|
|
622
684
|
|
|
623
685
|
**Get a Payment Setup Session:**
|
|
624
686
|
|
|
625
|
-
|
|
687
|
+
|
|
688
|
+
```typescript Store SDK
|
|
626
689
|
const session = await client.customer.paymentSetupSessions.get('pss_abc123', options)
|
|
627
690
|
```
|
|
628
691
|
|
|
692
|
+
```bash cURL
|
|
693
|
+
curl 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_sessions/pss_abc123' \
|
|
694
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
695
|
+
-H 'Authorization: Bearer $CUSTOMER_JWT'
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
|
|
629
699
|
**Complete a Payment Setup Session** (after the customer completes setup on the frontend using the gateway SDK and `external_client_secret`):
|
|
630
700
|
|
|
631
|
-
|
|
701
|
+
|
|
702
|
+
```typescript Store SDK
|
|
632
703
|
const completed = await client.customer.paymentSetupSessions.complete(
|
|
633
704
|
'pss_abc123',
|
|
634
705
|
{ external_data: {} },
|
|
@@ -638,6 +709,15 @@ console.log(completed.status) // 'completed'
|
|
|
638
709
|
console.log(completed.payment_source_id) // e.g. 'card_xyz789' - the saved payment method
|
|
639
710
|
```
|
|
640
711
|
|
|
712
|
+
```bash cURL
|
|
713
|
+
curl -X POST 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_sessions/pss_abc123/complete' \
|
|
714
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
715
|
+
-H 'Authorization: Bearer $CUSTOMER_JWT' \
|
|
716
|
+
-H 'Content-Type: application/json' \
|
|
717
|
+
-d '{ "external_data": {} }'
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
|
|
641
721
|
Spree will verify the result with the provider and create a saved payment source (e.g., `Spree::CreditCard`) that can be used for future payments.
|
|
642
722
|
|
|
643
723
|
## Supported Gateways
|
|
@@ -678,14 +758,6 @@ Spree publishes events throughout the payment lifecycle that you can subscribe t
|
|
|
678
758
|
|
|
679
759
|
See [Events](events.md) for more details on subscribing to events.
|
|
680
760
|
|
|
681
|
-
## Related Documentation
|
|
682
|
-
|
|
683
|
-
- [Payments (Store SDK)](../sdk/store/payments.md) - SDK how-to for payment sessions, payments, and setup sessions
|
|
684
|
-
- [Build a Custom Payment Method](../how-to/custom-payment-method.md) - Step-by-step guide to creating your own payment gateway integration
|
|
685
|
-
- [Orders](orders.md) - Order lifecycle, payment and fulfillment status
|
|
686
|
-
- [Checkout Customization](carts.md) - Customizing the checkout flow
|
|
687
|
-
- [Events](events.md) - Subscribe to payment events
|
|
688
|
-
|
|
689
761
|
## Two paths to a completed order
|
|
690
762
|
|
|
691
763
|
A payment can finish in two places, and both have to end at the same result.
|
|
@@ -697,3 +769,11 @@ Whichever arrives first completes the order; the other finds the work already do
|
|
|
697
769
|
> **WARNING:** Never treat the browser returning from a redirect as proof of payment. The provider's webhook is the authoritative signal — a customer can close the tab, and a browser response can be forged.
|
|
698
770
|
|
|
699
771
|
Both paths can be replaced if your integration needs different behaviour — see [Dependencies](../customization/dependencies.md).
|
|
772
|
+
|
|
773
|
+
## Related Documentation
|
|
774
|
+
|
|
775
|
+
- [Payments (Store SDK)](../sdk/store/payments.md) - SDK how-to for payment sessions, payments, and setup sessions
|
|
776
|
+
- [Build a Custom Payment Method](../how-to/custom-payment-method.md) - Step-by-step guide to creating your own payment gateway integration
|
|
777
|
+
- [Orders](orders.md) - Order lifecycle, payment and fulfillment status
|
|
778
|
+
- [Checkout Customization](carts.md) - Customizing the checkout flow
|
|
779
|
+
- [Events](events.md) - Subscribe to payment events
|
|
@@ -268,7 +268,7 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=prior_pric
|
|
|
268
268
|
|
|
269
269
|
The response includes a `prior_price` object when expanded:
|
|
270
270
|
|
|
271
|
-
```json
|
|
271
|
+
```json Response
|
|
272
272
|
{
|
|
273
273
|
"id": "prod_xxx",
|
|
274
274
|
"name": "Spree Tote",
|
|
@@ -287,15 +287,20 @@ The response includes a `prior_price` object when expanded:
|
|
|
287
287
|
|
|
288
288
|
Price history tracking is enabled by default. To disable it (e.g., for non-EU stores):
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
|
|
291
|
+
```typescript Admin SDK
|
|
292
|
+
await client.store.update({ preferred_track_price_history: false })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```bash cURL
|
|
296
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
|
|
297
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
298
|
+
-H 'Content-Type: application/json' \
|
|
299
|
+
-d '{ "preferred_track_price_history": false }'
|
|
296
300
|
```
|
|
297
301
|
|
|
298
|
-
|
|
302
|
+
|
|
303
|
+
Retention defaults to 30 days, set per store with the `price_history_retention_days` preference. A Rake task prunes anything older:
|
|
299
304
|
|
|
300
305
|
|
|
301
306
|
```bash Spree CLI (Docker)
|
|
@@ -213,6 +213,7 @@ spree api post /products -d '{
|
|
|
213
213
|
|
|
214
214
|
For something with no options at all, send prices directly and skip the variants array — Spree forwards them to the product's single variant:
|
|
215
215
|
|
|
216
|
+
|
|
216
217
|
```typescript Admin SDK
|
|
217
218
|
await adminClient.products.create({
|
|
218
219
|
name: 'The Spree Handbook',
|
|
@@ -221,6 +222,18 @@ await adminClient.products.create({
|
|
|
221
222
|
})
|
|
222
223
|
```
|
|
223
224
|
|
|
225
|
+
```bash cURL
|
|
226
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/products' \
|
|
227
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
228
|
+
-H 'Content-Type: application/json' \
|
|
229
|
+
-d '{
|
|
230
|
+
"name": "The Spree Handbook",
|
|
231
|
+
"status": "active",
|
|
232
|
+
"prices": [{ "currency": "USD", "amount": "19.99" }]
|
|
233
|
+
}'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
|
|
224
237
|
Don't pass both.
|
|
225
238
|
|
|
226
239
|
Update, clone, or archive a product (deleting soft-deletes it):
|
|
@@ -346,7 +359,7 @@ await client.products.approve('prod_xxx')
|
|
|
346
359
|
await client.products.reject('prod_xxx', { reason: 'Please add a photo showing scale.' })
|
|
347
360
|
|
|
348
361
|
// The review queue
|
|
349
|
-
const pending = await client.products.list({
|
|
362
|
+
const pending = await client.products.list({ status_eq: 'proposed' })
|
|
350
363
|
|
|
351
364
|
// Who decided, and when
|
|
352
365
|
const product = await client.products.get('prod_xxx', { expand: ['submission'] })
|
|
@@ -448,6 +461,7 @@ There's no such thing as a product without one. A book with no size or colour st
|
|
|
448
461
|
|
|
449
462
|
`default_variant_id` names the variant that represents the product — the price shown on a listing page, and what "add to cart" means before anyone picks anything.
|
|
450
463
|
|
|
464
|
+
|
|
451
465
|
```typescript Store SDK
|
|
452
466
|
const product = await client.products.get('spree-tote')
|
|
453
467
|
|
|
@@ -456,6 +470,12 @@ product.price // that variant's price
|
|
|
456
470
|
product.variant_count // 6
|
|
457
471
|
```
|
|
458
472
|
|
|
473
|
+
```bash cURL
|
|
474
|
+
curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
|
|
475
|
+
-H 'X-Spree-API-Key: pk_xxx'
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
|
|
459
479
|
For a single-variant product that's the only variant. For a product with options it's the first one, unless you say otherwise. If the default is ever removed, another is promoted automatically — a product is never left without one.
|
|
460
480
|
|
|
461
481
|
Products can also carry `buy_box_variant_id`, naming the variant a marketplace has chosen to feature when several sellers offer the same listing.
|
|
@@ -583,6 +603,7 @@ erDiagram
|
|
|
583
603
|
}
|
|
584
604
|
```
|
|
585
605
|
|
|
606
|
+
|
|
586
607
|
```typescript Admin SDK
|
|
587
608
|
const shoes = await adminClient.productTypes.create({
|
|
588
609
|
name: 'Footwear',
|
|
@@ -601,6 +622,19 @@ await adminClient.products.create({
|
|
|
601
622
|
})
|
|
602
623
|
```
|
|
603
624
|
|
|
625
|
+
```bash cURL
|
|
626
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/product_types' \
|
|
627
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
628
|
+
-H 'Content-Type: application/json' \
|
|
629
|
+
-d '{
|
|
630
|
+
"name": "Footwear",
|
|
631
|
+
"option_type_ids": ["optt_size", "optt_color"],
|
|
632
|
+
"category_ids": ["ctg_footwear"],
|
|
633
|
+
"delivery_profile_id": "dp_standard"
|
|
634
|
+
}'
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
|
|
604
638
|
### A type is a template, not a controller
|
|
605
639
|
|
|
606
640
|
This is the part that determines how you should think about them.
|
|
@@ -620,10 +654,17 @@ Seeding is also **additive** — it adds what's missing and never removes what a
|
|
|
620
654
|
|
|
621
655
|
If you *do* want an edited type to reach the products already using it, that's an explicit action:
|
|
622
656
|
|
|
657
|
+
|
|
623
658
|
```typescript Admin SDK
|
|
624
659
|
const { products_count } = await adminClient.productTypes.applyToProducts('pt_xxx')
|
|
625
660
|
```
|
|
626
661
|
|
|
662
|
+
```bash cURL
|
|
663
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/product_types/pt_xxx/apply_to_products' \
|
|
664
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
|
|
627
668
|
It runs in the background and is additive like the rest — never a side effect of saving a type.
|
|
628
669
|
|
|
629
670
|
> **INFO:** `required` on a type's custom field is **advisory** — it marks the field in the dashboard but isn't enforced on write, since Spree saves the product and its fields in two steps. Validate in your own tooling if you need it enforced.
|
|
@@ -878,6 +919,7 @@ curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts/products?l
|
|
|
878
919
|
|
|
879
920
|
Collections work the same way from a storefront's point of view:
|
|
880
921
|
|
|
922
|
+
|
|
881
923
|
```typescript Store SDK
|
|
882
924
|
const { data: collections } = await client.collections.list()
|
|
883
925
|
|
|
@@ -886,6 +928,15 @@ const { data: products } = await client.collections.products.list('summer-2025',
|
|
|
886
928
|
})
|
|
887
929
|
```
|
|
888
930
|
|
|
931
|
+
```bash cURL
|
|
932
|
+
curl 'https://api.mystore.com/api/v3/store/collections' \
|
|
933
|
+
-H 'X-Spree-API-Key: pk_xxx'
|
|
934
|
+
|
|
935
|
+
curl 'https://api.mystore.com/api/v3/store/collections/summer-2025/products?limit=12' \
|
|
936
|
+
-H 'X-Spree-API-Key: pk_xxx'
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
|
|
889
940
|
A rule-based collection is defined once and maintains itself — set it to match everything tagged `sale` and under $50, and products appear and disappear as those facts change. Ordering can be manual or by a rule such as newest first.
|
|
890
941
|
|
|
891
942
|
## Publications and Sales Channels
|
|
@@ -28,7 +28,7 @@ flowchart LR
|
|
|
28
28
|
|
|
29
29
|
A query names what to measure and how to slice it. This is the whole surface:
|
|
30
30
|
|
|
31
|
-
```json
|
|
31
|
+
```json Query
|
|
32
32
|
{
|
|
33
33
|
"metrics": ["total_sales", "orders"],
|
|
34
34
|
"dimensions": [{ "name": "completed_at", "grain": "week" }],
|
|
@@ -76,7 +76,7 @@ different clocks, so a payment total beside a units-received count is two
|
|
|
76
76
|
reports wearing one table — the query refuses the mix rather than inventing a
|
|
77
77
|
join between grains that have no honest relationship.
|
|
78
78
|
|
|
79
|
-
```ruby
|
|
79
|
+
```ruby server/config/initializers/spree.rb
|
|
80
80
|
Spree.reporting.metric :total_sales,
|
|
81
81
|
sql: 'SUM(%{orders}.total)', base: :orders, format: :money
|
|
82
82
|
```
|
|
@@ -118,7 +118,7 @@ row and inflate every other metric in the same query.
|
|
|
118
118
|
Core's sales metrics follow the sequence merchants and accountants already use,
|
|
119
119
|
so a Spree figure means what the same word means everywhere else:
|
|
120
120
|
|
|
121
|
-
```
|
|
121
|
+
```text
|
|
122
122
|
gross_sales − discounts − returns = net_sales
|
|
123
123
|
net_sales + shipping + duties + fees + taxes = total_sales
|
|
124
124
|
```
|
|
@@ -137,7 +137,7 @@ zero and margin over an incompletely-costed catalogue reads high.
|
|
|
137
137
|
A dimension groups and filters. Its definition owns every behaviour keyed off
|
|
138
138
|
it, which is what lets an extension-registered dimension work end to end:
|
|
139
139
|
|
|
140
|
-
```ruby
|
|
140
|
+
```ruby server/config/initializers/spree.rb
|
|
141
141
|
Spree.reporting.dimension :channel, base: :orders, column: :channel_id,
|
|
142
142
|
lookup: :channel,
|
|
143
143
|
subject: -> { Spree::Channel }, key_scope: 'read_channels',
|
|
@@ -179,7 +179,7 @@ fulfill", "Open returns" and "Low stock" are the state of the store right now,
|
|
|
179
179
|
with no time range and no currency. The registry holds these as **counters**
|
|
180
180
|
beside metrics and dimensions, and `GET /dashboard/counters` evaluates them:
|
|
181
181
|
|
|
182
|
-
```json
|
|
182
|
+
```json Response
|
|
183
183
|
{
|
|
184
184
|
"channel_id": null,
|
|
185
185
|
"counters": [
|