@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.
Files changed (46) hide show
  1. package/dist/api-reference/store.yaml +28 -0
  2. package/dist/developer/agentic/agent-skills.md +1 -1
  3. package/dist/developer/cli/quickstart.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +13 -0
  5. package/dist/developer/core-concepts/calculators.md +6 -1
  6. package/dist/developer/core-concepts/carts.md +19 -1
  7. package/dist/developer/core-concepts/catalogs.md +51 -2
  8. package/dist/developer/core-concepts/channels.md +20 -1
  9. package/dist/developer/core-concepts/commissions.md +70 -1
  10. package/dist/developer/core-concepts/companies.md +74 -0
  11. package/dist/developer/core-concepts/customers.md +17 -5
  12. package/dist/developer/core-concepts/data-privacy.md +20 -7
  13. package/dist/developer/core-concepts/delivery-setup.md +57 -0
  14. package/dist/developer/core-concepts/discounts.md +7 -0
  15. package/dist/developer/core-concepts/events.md +13 -7
  16. package/dist/developer/core-concepts/fees.md +20 -0
  17. package/dist/developer/core-concepts/fulfillments.md +3 -3
  18. package/dist/developer/core-concepts/imports-exports.md +21 -5
  19. package/dist/developer/core-concepts/inventory.md +73 -1
  20. package/dist/developer/core-concepts/markets.md +5 -6
  21. package/dist/developer/core-concepts/media.md +66 -1
  22. package/dist/developer/core-concepts/metafields.md +1 -1
  23. package/dist/developer/core-concepts/order-totals.md +18 -1
  24. package/dist/developer/core-concepts/orders.md +2 -1
  25. package/dist/developer/core-concepts/payments.md +100 -20
  26. package/dist/developer/core-concepts/pricing.md +13 -8
  27. package/dist/developer/core-concepts/products.md +52 -1
  28. package/dist/developer/core-concepts/reporting.md +5 -5
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +41 -2
  30. package/dist/developer/core-concepts/search-filtering.md +1 -2
  31. package/dist/developer/core-concepts/sellers.md +109 -2
  32. package/dist/developer/core-concepts/slugs.md +1 -1
  33. package/dist/developer/core-concepts/staff-roles.md +23 -3
  34. package/dist/developer/core-concepts/store-credits-gift-cards.md +60 -23
  35. package/dist/developer/core-concepts/stores.md +21 -0
  36. package/dist/developer/core-concepts/taxes.md +43 -0
  37. package/dist/developer/core-concepts/translations.md +9 -0
  38. package/dist/developer/core-concepts/webhooks.md +45 -3
  39. package/dist/developer/create-spree-app/quickstart.md +10 -10
  40. package/dist/developer/dashboard/deployment.md +4 -4
  41. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +1 -1
  42. package/dist/developer/deployment/docker.md +2 -2
  43. package/dist/developer/how-to/custom-promotion.md +33 -13
  44. package/dist/developer/multi-tenant/quickstart.md +6 -6
  45. package/dist/developer/storefront/nextjs/testing.md +2 -2
  46. 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
- filter: { payment_status_eq: 'paid', fulfillment_status_eq: 'unfulfilled' },
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```typescript
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
- ```ruby
291
- # config/initializers/spree.rb
292
- Spree.config do |config|
293
- config.track_price_history = false
294
- config.price_history_retention_days = 30 # default
295
- end
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
- Price history retention defaults to 30 days and can be configured globally. A Rake task is provided for cleanup:
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({ filter: { status_eq: 'proposed' } })
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": [