@spree/docs 0.1.93 → 0.1.95

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 (47) hide show
  1. package/dist/api-reference/admin-api/authentication.md +8 -7
  2. package/dist/api-reference/admin-api/endpoints.md +348 -0
  3. package/dist/api-reference/admin-api/errors.md +2 -0
  4. package/dist/api-reference/admin-api/introduction.md +11 -0
  5. package/dist/api-reference/store.yaml +243 -232
  6. package/dist/developer/agentic/overview.md +1 -1
  7. package/dist/developer/cli/admin-api.md +146 -0
  8. package/dist/developer/cli/quickstart.md +40 -5
  9. package/dist/developer/core-concepts/addresses.md +32 -16
  10. package/dist/developer/core-concepts/adjustments.md +11 -5
  11. package/dist/developer/core-concepts/architecture.md +8 -8
  12. package/dist/developer/core-concepts/calculators.md +31 -51
  13. package/dist/developer/core-concepts/channels.md +13 -6
  14. package/dist/developer/core-concepts/customers.md +47 -23
  15. package/dist/developer/core-concepts/events.md +22 -17
  16. package/dist/developer/core-concepts/imports-exports.md +69 -14
  17. package/dist/developer/core-concepts/inventory.md +79 -1
  18. package/dist/developer/core-concepts/markets.md +64 -20
  19. package/dist/developer/core-concepts/media.md +43 -6
  20. package/dist/developer/core-concepts/metafields.md +76 -13
  21. package/dist/developer/core-concepts/orders.md +95 -17
  22. package/dist/developer/core-concepts/payments.md +14 -13
  23. package/dist/developer/core-concepts/pricing.md +95 -9
  24. package/dist/developer/core-concepts/products.md +192 -26
  25. package/dist/developer/core-concepts/promotions.md +61 -4
  26. package/dist/developer/core-concepts/reports.md +4 -2
  27. package/dist/developer/core-concepts/search-filtering.md +82 -32
  28. package/dist/developer/core-concepts/shipments.md +16 -13
  29. package/dist/developer/core-concepts/slugs.md +20 -11
  30. package/dist/developer/core-concepts/staff-roles.md +51 -1
  31. package/dist/developer/core-concepts/store-credits-gift-cards.md +90 -9
  32. package/dist/developer/core-concepts/stores.md +16 -14
  33. package/dist/developer/core-concepts/taxes.md +28 -0
  34. package/dist/developer/core-concepts/translations.md +16 -7
  35. package/dist/developer/core-concepts/users.md +13 -9
  36. package/dist/developer/core-concepts/webhooks.md +95 -64
  37. package/dist/developer/getting-started/quickstart.md +20 -0
  38. package/dist/developer/how-to/custom-api-authentication.md +103 -23
  39. package/dist/developer/multi-store/quickstart.md +1 -1
  40. package/dist/developer/sdk/admin/authentication.md +1 -1
  41. package/dist/developer/sdk/admin/resources.md +2 -0
  42. package/dist/developer/upgrades/5.3-to-5.4.md +1 -1
  43. package/dist/developer/upgrades/5.4-to-5.5.md +1 -1
  44. package/dist/integrations/integrations.md +0 -7
  45. package/package.json +1 -1
  46. package/dist/integrations/sso-mfa-social-login/admin-dashboard.md +0 -57
  47. package/dist/integrations/sso-mfa-social-login/storefront.md +0 -56
@@ -77,7 +77,7 @@ erDiagram
77
77
 
78
78
  Refund {
79
79
  decimal amount
80
- string reason
80
+ integer refund_reason_id
81
81
  }
82
82
 
83
83
  LogEntry {
@@ -128,7 +128,7 @@ A `PaymentMethod` can have the following attributes:
128
128
  | `name` | The visible name for this payment method | `Check` |
129
129
  | `description`| The description for this payment method | `Pay by check.` |
130
130
  | `active` | Whether or not this payment method is active. Set it `false` to hide it in the Store API. | `true` |
131
- | `display_on` | Determines where the payment method can be visible. Values can be `front` for Store API, `back` for admin panel only or `both` for both. | `both` |
131
+ | `display_on` | Determines where the payment method can be visible. Values can be `front_end` (Store API), `back_end` (admin panel only), or `both` (both). | `both` |
132
132
  | `position` | The position of the payment method in lists. Lower numbers appear first. | `1` |
133
133
 
134
134
  > **INFO:** Each payment method is associated to a Store, so you can decide which Payment Method will appear on which Store. This allows you to create different experiences for your customers in different countries.
@@ -227,7 +227,7 @@ The frontend calls the API to create a Payment Session for a specific payment me
227
227
 
228
228
  **Step 2: Customer pays on the frontend**
229
229
 
230
- The frontend uses the gateway's JavaScript SDK (e.g., Stripe.js, Adyen Drop-in) with the `client_secret` to securely collect payment details. Card data never touches your server — it goes directly to the payment provider, ensuring **PCI compliance**. If the payment requires **3D Secure** authentication or redirects to an offsite gateway (CashApp, Klarna, etc.), the gateway SDK handles it automatically.
230
+ The frontend uses the gateway's JavaScript SDK (e.g., Stripe.js, Adyen Drop-in) with the `client_secret` to securely collect payment details. Card data never touches your server — it goes directly to the payment provider, ensuring [PCI compliance](../security/pci_compliance.md). If the payment requires **3D Secure** authentication or redirects to an offsite gateway (CashApp, Klarna, etc.), the gateway SDK handles it automatically.
231
231
 
232
232
  **Step 3: Complete Payment Session**
233
233
 
@@ -291,14 +291,14 @@ sequenceDiagram
291
291
  participant Frontend
292
292
  participant Spree API
293
293
 
294
- Frontend->>Spree API: GET /payment_methods
295
- Spree API-->>Frontend: Payment methods (with session_required flag)
294
+ Frontend->>Spree API: GET /carts/:id
295
+ Spree API-->>Frontend: Cart (with embedded payment_methods, each carrying session_required)
296
296
 
297
297
  Frontend->>Spree API: POST /payments (payment_method_id)
298
298
  Spree API->>Spree API: Create Payment (checkout state)
299
299
  Spree API-->>Frontend: Payment created
300
300
 
301
- Frontend->>Spree API: PATCH /orders/:id/complete
301
+ Frontend->>Spree API: POST /carts/:id/complete
302
302
  Spree API->>Spree API: process_payments! (authorize succeeds immediately)
303
303
  Spree API->>Spree API: Payment → pending/completed
304
304
  Spree API-->>Frontend: Order completed
@@ -306,7 +306,7 @@ sequenceDiagram
306
306
 
307
307
  **Step 1: List payment methods**
308
308
 
309
- The frontend calls `GET /payment_methods` and checks the `session_required` flag on each method. Methods with `session_required: false` use this direct flow.
309
+ The frontend reads the cart's embedded `payment_methods` (returned by `GET /carts/:id`) and checks the `session_required` flag on each method. Methods with `session_required: false` use this direct flow.
310
310
 
311
311
  **Step 2: Create payment**
312
312
 
@@ -461,7 +461,7 @@ Once a Payment Session is completed, Spree creates a `Payment` record (`Spree::P
461
461
 
462
462
  | Attribute | Description | Example Value |
463
463
  |-------------------|-----------------------------------------------------------------------------|---------------------|
464
- | `number` | A unique identifier for the payment. | `P123456789` |
464
+ | `number` | A unique identifier for the payment. | `PAB12CD34` |
465
465
  | `source_type` | The type of source used for the payment. | `Spree::CreditCard` |
466
466
  | `source_id` | The ID of the source used for the payment. | `1` |
467
467
  | `amount` | The amount of the payment. | `99.99` |
@@ -590,12 +590,12 @@ sequenceDiagram
590
590
  | `external_id` | The provider-side session ID (e.g., Stripe SetupIntent ID) | `seti_ABC123` |
591
591
  | `external_client_secret` | Client secret for the frontend SDK | `seti_ABC123_secret_xyz` |
592
592
  | `external_data` | Provider-specific data | `{}` |
593
- | `payment_source_id` | The saved payment source created after completion | `ps_xyz789` |
593
+ | `payment_source_id` | The saved payment source created after completion | `card_xyz789` |
594
594
  | `payment_source_type` | The type of saved payment source | `Spree::CreditCard` |
595
595
 
596
596
  ### Payment Setup Session API
597
597
 
598
- > **NOTE:** Payment Setup Sessions require customer authentication. The customer must be logged in.
598
+ > **NOTE:** Payment Setup Sessions require [customer authentication](../../api-reference/store-api/authentication.md). The customer must be logged in.
599
599
 
600
600
  **Create a Payment Setup Session:**
601
601
 
@@ -624,7 +624,7 @@ console.log(setupSession.external_client_secret) // e.g. 'seti_ABC123_secret_xyz
624
624
  "payment_method_id": "pm_xyz789",
625
625
  "payment_source_id": null,
626
626
  "payment_source_type": null,
627
- "customer_id": "usr_def456"
627
+ "customer_id": "cus_def456"
628
628
  }
629
629
  ```
630
630
 
@@ -643,7 +643,7 @@ const completed = await client.customer.paymentSetupSessions.complete(
643
643
  options
644
644
  )
645
645
  console.log(completed.status) // 'completed'
646
- console.log(completed.payment_source_id) // e.g. 'ps_xyz789' - the saved payment method
646
+ console.log(completed.payment_source_id) // e.g. 'card_xyz789' - the saved payment method
647
647
  ```
648
648
 
649
649
  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.
@@ -660,7 +660,7 @@ Spree team maintains several payment gateway integrations. All of these gateways
660
660
 
661
661
  ## Payment Events
662
662
 
663
- Spree publishes events throughout the payment lifecycle that you can subscribe to:
663
+ Spree publishes events throughout the payment lifecycle that you can subscribe to. For the delivered payload schemas of these events (e.g. `payment.paid`, `payment_session.completed`), see the [Webhooks & Events reference](../../api-reference/webhooks-events.md):
664
664
 
665
665
  ### Payment Events
666
666
  | Event | Description |
@@ -688,6 +688,7 @@ See [Events](events.md) for more details on subscribing to events.
688
688
 
689
689
  ## Related Documentation
690
690
 
691
+ - [Payments (Store SDK)](../sdk/store/payments.md) - SDK how-to for payment sessions, payments, and setup sessions
691
692
  - [Build a Custom Payment Method](../how-to/custom-payment-method.md) - Step-by-step guide to creating your own payment gateway integration
692
693
  - [Orders](orders.md) - Order management and state machine
693
694
  - [Checkout Customization](../customization/checkout.md) - Customizing the checkout flow
@@ -11,7 +11,7 @@ Spree's pricing system supports both simple single-currency pricing and advanced
11
11
 
12
12
  ## Prices
13
13
 
14
- Each variant has one or more `Price` records — one per currency. The API automatically returns the correct price based on the current currency and [Market](markets.md) context.
14
+ Each variant has one or more `Price` records — one per currency. The API automatically returns the correct price based on the current currency and [Market](markets.md) context. See [monetary amounts](../../api-reference/store-api/monetary-amounts.md) for how money fields (decimal, cents, and display string) are formatted across the API.
15
15
 
16
16
  | Attribute | Description | Example |
17
17
  |-----------|-------------|---------|
@@ -26,19 +26,25 @@ Each variant has one or more `Price` records — one per currency. The API autom
26
26
  The Store API returns the resolved price (including Price List rules) for the current currency and market context:
27
27
 
28
28
 
29
- ```typescript SDK
29
+ ```typescript Store SDK
30
30
  // Product price is included by default
31
31
  const product = await client.products.get('spree-tote')
32
- console.log(product.price) // "15.99" — resolved for current currency
33
- console.log(product.original_price) // "19.99" — compare-at price (if set)
32
+ console.log(product.price.amount) // "15.99" — resolved for current currency
33
+ console.log(product.price.compare_at_amount) // "19.99" — compare-at/strikethrough price (if set)
34
34
 
35
35
  // Each variant has its own price
36
36
  const detailed = await client.products.get('spree-tote', {
37
37
  expand: ['variants'],
38
38
  })
39
39
  detailed.variants?.forEach(variant => {
40
- console.log(variant.price) // "15.99"
41
- console.log(variant.original_price) // "19.99"
40
+ console.log(variant.price.amount) // "15.99"
41
+ console.log(variant.price.compare_at_amount) // "19.99"
42
+ })
43
+ ```
44
+
45
+ ```typescript Admin SDK
46
+ const product = await adminClient.products.get('prod_86Rf07xd4z', {
47
+ expand: ['variants'],
42
48
  })
43
49
  ```
44
50
 
@@ -53,6 +59,41 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=variants'
53
59
  ```
54
60
 
55
61
 
62
+ ### Setting Prices via the Admin API
63
+
64
+ Base prices are set per variant per currency. Use the [Admin API](../../api-reference/admin-api/introduction.md) — via the [Admin SDK](../sdk/admin/quickstart.md) — to set them individually or in bulk (`bulk_upsert` matches on variant + currency):
65
+
66
+
67
+ ```typescript Admin SDK
68
+ import { createAdminClient } from '@spree/admin-sdk'
69
+
70
+ const client = createAdminClient({
71
+ baseUrl: 'https://store.example.com',
72
+ secretKey: 'sk_xxx',
73
+ })
74
+
75
+ // Set / change one variant's price in a currency
76
+ await client.prices.create({ variant_id: 'variant_xxx', currency: 'USD', amount: 15.99 })
77
+
78
+ // Upsert many at once
79
+ await client.prices.bulkUpsert({
80
+ prices: [
81
+ { variant_id: 'variant_xxx', currency: 'USD', amount: 15.99 },
82
+ { variant_id: 'variant_xxx', currency: 'EUR', amount: 14.99 },
83
+ ],
84
+ })
85
+ ```
86
+
87
+ ```bash CLI
88
+ spree api post /prices/bulk_upsert -d '{
89
+ "prices": [
90
+ { "variant_id": "variant_xxx", "currency": "USD", "amount": 15.99 },
91
+ { "variant_id": "variant_xxx", "currency": "EUR", "amount": 14.99 }
92
+ ]
93
+ }'
94
+ ```
95
+
96
+
56
97
  ## Price Lists
57
98
 
58
99
  Price Lists allow you to create different pricing strategies based on various conditions. This enables advanced pricing scenarios like:
@@ -86,6 +127,44 @@ If no applicable Price List is found, the base price is used.
86
127
  | `match_policy` | How rules are evaluated: `all` (every rule must match) or `any` (at least one) |
87
128
  | `position` | Priority order (lower numbers = higher priority) |
88
129
 
130
+ ### Creating a Price List via the Admin API
131
+
132
+ Rules and per-variant price overrides ride on the create/update payload. Each rule is a `{ type, preferences }` draft (see [Price Rules](#price-rules) below):
133
+
134
+
135
+ ```typescript Admin SDK
136
+ import { createAdminClient } from '@spree/admin-sdk'
137
+
138
+ const client = createAdminClient({
139
+ baseUrl: 'https://store.example.com',
140
+ secretKey: 'sk_xxx',
141
+ })
142
+
143
+ const priceList = await client.priceLists.create({
144
+ name: 'EU wholesale',
145
+ match_policy: 'all',
146
+ rules: [
147
+ { type: 'customer_group_rule', preferences: { customer_group_ids: ['cg_xxx'] } },
148
+ ],
149
+ prices: [
150
+ { variant_id: 'variant_xxx', currency: 'EUR', amount: '19.99' },
151
+ ],
152
+ })
153
+
154
+ // Price Lists are draft until activated
155
+ await client.priceLists.activate(priceList.id)
156
+ ```
157
+
158
+ ```bash CLI
159
+ spree api post /price_lists -d '{
160
+ "name": "EU wholesale",
161
+ "match_policy": "all",
162
+ "prices": [{ "variant_id": "variant_xxx", "currency": "EUR", "amount": "19.99" }]
163
+ }'
164
+ spree api patch /price_lists/pl_xxx/activate
165
+ ```
166
+
167
+
89
168
  ## Price Rules
90
169
 
91
170
  Price Rules define conditions that must be met for a Price List to apply. Spree includes five built-in rule types:
@@ -141,9 +220,9 @@ When resolving prices, Spree considers the full context of the request:
141
220
  | Quantity | Cart line item | The quantity for volume-based rules |
142
221
  | Date | Current time | For time-based Price List scheduling |
143
222
 
144
- The Store API automatically builds this context from the request headers (`X-Spree-Currency`, `X-Spree-Country`) and authentication state. You don't need to construct it manually — just make API requests and the correct price is resolved.
223
+ The Store API automatically builds this context from the [request headers](../../api-reference/store-api/localization.md) (`X-Spree-Currency`, `X-Spree-Country`) and authentication state. You don't need to construct it manually — just make API requests and the correct price is resolved.
145
224
 
146
- > **INFO:** Price resolution results are cached for 15 minutes. Different context combinations are cached separately.
225
+ > **INFO:** Price resolution runs per request there is no in-process caching of resolved prices. The Store API instead relies on HTTP/CDN caching, with `Vary` headers keyed on the `X-Spree-Currency` and `X-Spree-Locale` request headers so each currency/locale combination is cached separately at the edge.
147
226
 
148
227
  ## Time-Based Pricing
149
228
 
@@ -170,7 +249,7 @@ Every time a base price amount changes, a `PriceHistory` record is created autom
170
249
  The prior price is available as an expandable field on product and variant endpoints:
171
250
 
172
251
 
173
- ```typescript SDK
252
+ ```typescript Store SDK
174
253
  const product = await client.products.get('spree-tote', {
175
254
  expand: ['prior_price'],
176
255
  })
@@ -183,6 +262,12 @@ if (product.prior_price) {
183
262
  }
184
263
  ```
185
264
 
265
+ ```typescript Admin SDK
266
+ const product = await adminClient.products.get('prod_86Rf07xd4z', {
267
+ expand: ['prior_price'],
268
+ })
269
+ ```
270
+
186
271
  ```bash cURL
187
272
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=prior_price' \
188
273
  -H 'X-Spree-API-Key: pk_xxx'
@@ -247,6 +332,7 @@ bundle exec rake spree:price_history:seed
247
332
  ## Related Documentation
248
333
 
249
334
  - [Products](products.md) — Products, Variants, and base prices
335
+ - [Store SDK: Products](../sdk/store/products.md) — Fetching product and variant prices with the Store SDK
250
336
  - [Markets](markets.md) — Geographic regions with currency and locale
251
337
  - [Taxes](taxes.md) — Tax categories, tax rates, and zones
252
338
  - [Promotions](promotions.md) — Discount-based pricing via promotion rules
@@ -79,7 +79,7 @@ erDiagram
79
79
  ## Listing Products
80
80
 
81
81
 
82
- ```typescript SDK
82
+ ```typescript Store SDK
83
83
  // List products with pagination
84
84
  const { data: products, meta } = await client.products.list({
85
85
  limit: 12,
@@ -100,7 +100,14 @@ const results = await client.products.list({
100
100
 
101
101
  // Sort products
102
102
  const sorted = await client.products.list({
103
- sort: 'price_high_to_low', // or: price_low_to_high, newest, name_a_z, name_z_a
103
+ sort: '-price', // high to low; also: price, name, -name, available_on, -available_on, best_selling
104
+ })
105
+ ```
106
+
107
+ ```typescript Admin SDK
108
+ const { data: products, meta } = await adminClient.products.list({
109
+ limit: 12,
110
+ page: 1,
104
111
  })
105
112
  ```
106
113
 
@@ -124,7 +131,7 @@ See [Querying](../../api-reference/store-api/querying.md) for the full list of f
124
131
  ## Getting a Product
125
132
 
126
133
 
127
- ```typescript SDK
134
+ ```typescript Store SDK
128
135
  // Get by slug
129
136
  const product = await client.products.get('spree-tote')
130
137
 
@@ -133,8 +140,13 @@ const detailed = await client.products.get('spree-tote', {
133
140
  expand: ['variants', 'media', 'option_types', 'categories'],
134
141
  })
135
142
  // detailed.variants => [{ id: "var_xxx", sku: "TOTE-S-R", price: { amount: "15.99", currency: "USD" }, ... }]
136
- // detailed.media => [{ id: "img_xxx", url: "https://cdn...", position: 1 }]
137
- // detailed.option_types => [{ name: "size", presentation: "Size", option_values: [...] }]
143
+ // detailed.media => [{ id: "img_xxx", original_url: "https://cdn...", position: 1 }]
144
+ // detailed.option_types => [{ name: "size", label: "Size", position: 1, kind: "..." }]
145
+ // detailed.option_values => [{ name: "small", label: "S", option_type_name: "size", ... }] // separate top-level array when expanded
146
+ ```
147
+
148
+ ```typescript Admin SDK
149
+ const product = await adminClient.products.get('prod_86Rf07xd4z')
138
150
  ```
139
151
 
140
152
  ```bash cURL
@@ -143,17 +155,91 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=variants,m
143
155
  ```
144
156
 
145
157
 
158
+ Pass `expand` to include related resources in a single response — see [expand relations](../../api-reference/store-api/relations.md) for how relation inclusion works.
159
+
160
+ ## Managing Products
161
+
162
+ The examples above use the **Store API** (publishable key, read-only, customer-facing). To **create and manage** products, use the [Admin API](../../api-reference/admin-api/introduction.md) — via the [Admin SDK](../sdk/admin/quickstart.md) or the [Spree CLI](../cli/admin-api.md).
163
+
164
+ A product's purchasable attributes (SKU, prices, stock) live on its **variants**, which you can create inline:
165
+
166
+
167
+ ```typescript Admin SDK
168
+ import { createAdminClient } from '@spree/admin-sdk'
169
+
170
+ const client = createAdminClient({
171
+ baseUrl: 'https://store.example.com',
172
+ secretKey: 'sk_xxx',
173
+ })
174
+
175
+ const product = await client.products.create({
176
+ name: 'Premium T-Shirt',
177
+ description: 'Soft, organic cotton.',
178
+ status: 'active',
179
+ variants: [
180
+ {
181
+ sku: 'TSHIRT-S-NAVY',
182
+ options: [
183
+ { name: 'size', value: 'Small' },
184
+ { name: 'color', value: 'navy' },
185
+ ],
186
+ prices: [{ currency: 'USD', amount: 29.99 }],
187
+ stock_items: [{ stock_location_id: 'sloc_xxx', count_on_hand: 50 }],
188
+ },
189
+ ],
190
+ })
191
+ ```
192
+
193
+ ```bash CLI
194
+ spree api post /products -d '{
195
+ "name": "Premium T-Shirt",
196
+ "status": "active",
197
+ "variants": [{
198
+ "sku": "TSHIRT-S-NAVY",
199
+ "options": [{ "name": "size", "value": "Small" }],
200
+ "prices": [{ "currency": "USD", "amount": 29.99 }]
201
+ }]
202
+ }'
203
+ ```
204
+
205
+
206
+ Update, clone, or archive a product (deleting soft-deletes it):
207
+
208
+
209
+ ```typescript Admin SDK
210
+ await client.products.update('prod_xxx', { name: 'Premium Tee', status: 'active' })
211
+ await client.products.clone('prod_xxx') // duplicate as a new draft
212
+ await client.products.delete('prod_xxx') // soft-delete
213
+ ```
214
+
215
+ ```bash CLI
216
+ spree api patch /products/prod_xxx -d '{"name": "Premium Tee"}'
217
+ spree api post /products/prod_xxx/clone
218
+ spree api delete /products/prod_xxx
219
+ ```
220
+
221
+
222
+ > **TIP:** Operating on many products at once? The Admin API has bulk actions — `bulkStatusUpdate`, `bulkAddToCategories`, `bulkAddTags`, `bulkDestroy`, and more. See the [Admin API endpoint index](../../api-reference/admin-api/endpoints.md).
223
+
146
224
  ## Product Filters
147
225
 
148
226
  Get available filter options for building a faceted search UI. Returns price ranges, option values, and categories with counts:
149
227
 
150
228
 
151
- ```typescript SDK
229
+ ```typescript Store SDK
152
230
  const filters = await client.products.filters()
153
231
  // {
154
- // option_types: [{ name: "size", option_values: [{ name: "Small", count: 12 }, ...] }],
155
- // price_range: { min: 9.99, max: 199.99 },
156
- // categories: [{ id: "ctg_xxx", name: "Clothing", count: 45 }],
232
+ // filters: [
233
+ // { id: "price", type: "price_range", min: 9.99, max: 199.99, currency: "USD" },
234
+ // { id: "availability", type: "availability", options: [{ id: "in_stock", count: 42 }] },
235
+ // { id: "opt_xxx", type: "option", name: "size", label: "Size", kind: "...",
236
+ // options: [{ id: "optv_xxx", name: "small", label: "Small", count: 12 }, ...] },
237
+ // { id: "categories", type: "category",
238
+ // options: [{ id: "ctg_xxx", name: "Clothing", permalink: "clothing", count: 45 }] },
239
+ // ],
240
+ // sort_options: [{ id: "price" }, ...],
241
+ // default_sort: "best_selling",
242
+ // total_count: 120,
157
243
  // }
158
244
 
159
245
  // Scoped to a specific category
@@ -206,6 +292,30 @@ When a product has option types, each unique combination of option values create
206
292
 
207
293
  The product's `default_variant_id` points to the first non-master variant (or the master variant if none exist).
208
294
 
295
+ Add a variant to an existing product via the Admin API (SKU, prices, and stock all live on the variant):
296
+
297
+
298
+ ```typescript Admin SDK
299
+ const variant = await client.products.variants.create('prod_xxx', {
300
+ sku: 'TEE-L-R',
301
+ options: [
302
+ { name: 'size', value: 'Large' },
303
+ { name: 'color', value: 'Red' },
304
+ ],
305
+ prices: [{ currency: 'USD', amount: 24.99 }],
306
+ stock_items: [{ stock_location_id: 'sloc_xxx', count_on_hand: 30 }],
307
+ })
308
+ ```
309
+
310
+ ```bash CLI
311
+ spree api post /products/prod_xxx/variants -d '{
312
+ "sku": "TEE-L-R",
313
+ "options": [{ "name": "size", "value": "Large" }],
314
+ "prices": [{ "currency": "USD", "amount": 24.99 }]
315
+ }'
316
+ ```
317
+
318
+
209
319
  ## Option Types and Option Values
210
320
 
211
321
  Option types define the axes of variation for a product (e.g., Size, Color, Material). Option values are the specific choices within each type (e.g., Small, Medium, Large).
@@ -213,26 +323,62 @@ Option types define the axes of variation for a product (e.g., Size, Color, Mate
213
323
  A product must have at least one option type to have multiple variants. Option types and their values are included in the product response when requested:
214
324
 
215
325
 
216
- ```typescript SDK
326
+ ```typescript Store SDK
217
327
  const product = await client.products.get('spree-tee', {
218
- expand: ['option_types'],
328
+ expand: ['option_types', 'option_values'],
219
329
  })
220
330
 
331
+ // Option types describe the axes of variation
221
332
  product.option_types?.forEach(optionType => {
222
- console.log(optionType.presentation) // "Size"
223
- optionType.option_values.forEach(value => {
224
- console.log(value.presentation) // "Small", "Medium", "Large"
225
- })
333
+ console.log(optionType.label) // "Size"
334
+ })
335
+
336
+ // Option values are a separate flat array; each carries its parent's id/label
337
+ product.option_values?.forEach(value => {
338
+ console.log(value.option_type_label, value.label) // "Size", "Small"
339
+ })
340
+ ```
341
+
342
+ ```typescript Admin SDK
343
+ const product = await adminClient.products.get('prod_k5nR8xLq', {
344
+ expand: ['variants'],
226
345
  })
227
346
  ```
228
347
 
229
348
  ```bash cURL
230
- curl 'https://api.mystore.com/api/v3/store/products/spree-tee?expand=option_types' \
349
+ curl 'https://api.mystore.com/api/v3/store/products/spree-tee?expand=option_types,option_values' \
231
350
  -H 'X-Spree-API-Key: pk_xxx'
232
351
  ```
233
352
 
234
353
 
235
- > **INFO:** Option type `name` and `presentation` fields are translatable.
354
+ > **INFO:** Option type `name` and `label` fields are translatable.
355
+
356
+ Create option types (and their values) via the Admin API. Sending `option_values` replaces the full set, so include every value you want to keep:
357
+
358
+
359
+ ```typescript Admin SDK
360
+ const optionType = await client.optionTypes.create({
361
+ name: 'size',
362
+ label: 'Size',
363
+ option_values: [
364
+ { name: 'small', label: 'Small', position: 1 },
365
+ { name: 'medium', label: 'Medium', position: 2 },
366
+ { name: 'large', label: 'Large', position: 3 },
367
+ ],
368
+ })
369
+ ```
370
+
371
+ ```bash CLI
372
+ spree api post /option_types -d '{
373
+ "name": "size",
374
+ "label": "Size",
375
+ "option_values": [
376
+ { "name": "small", "label": "Small" },
377
+ { "name": "medium", "label": "Medium" }
378
+ ]
379
+ }'
380
+ ```
381
+
236
382
 
237
383
  ## Media
238
384
 
@@ -245,7 +391,7 @@ Every product response includes a `thumbnail_url` field — the URL to the first
245
391
  Use these fields for product listing pages to avoid loading all images:
246
392
 
247
393
 
248
- ```typescript SDK
394
+ ```typescript Store SDK
249
395
  // List products — thumbnail_url is always included
250
396
  const { data: products } = await client.products.list({ limit: 12 })
251
397
 
@@ -254,6 +400,10 @@ products.forEach(product => {
254
400
  })
255
401
  ```
256
402
 
403
+ ```typescript Admin SDK
404
+ const { data: products } = await adminClient.products.list({ limit: 12 })
405
+ ```
406
+
257
407
  ```bash cURL
258
408
  # thumbnail_url is always in the response — no ?expand needed
259
409
  curl 'https://api.mystore.com/api/v3/store/products?limit=12' \
@@ -268,22 +418,28 @@ curl 'https://api.mystore.com/api/v3/store/products?limit=12' \
268
418
  On the product detail page, expand `media` and `variants` to get the full set of images. Images are ordered by `position`:
269
419
 
270
420
 
271
- ```typescript SDK
421
+ ```typescript Store SDK
272
422
  const product = await client.products.get('spree-tote', {
273
423
  expand: ['media', 'variants'],
274
424
  })
275
425
 
276
426
  // Product-level images (from master variant)
277
- product.media // [{ url: "https://cdn.../tote-front.jpg", position: 1 }, ...]
427
+ product.media // [{ original_url: "https://cdn.../tote-front.jpg", position: 1 }, ...]
278
428
 
279
429
  // Each variant has its own thumbnail and media_count
280
430
  product.variants?.forEach(variant => {
281
- variant.thumbnail // "https://cdn.../tote-red.jpg" — always available
431
+ variant.thumbnail_url // "https://cdn.../tote-red.jpg" — always available
282
432
  variant.media_count // 3 — quick check without loading media
283
433
  variant.media // full image array (only when ?expand=media)
284
434
  })
285
435
  ```
286
436
 
437
+ ```typescript Admin SDK
438
+ const product = await adminClient.products.get('prod_86Rf07xd4z', {
439
+ expand: ['media'],
440
+ })
441
+ ```
442
+
287
443
  ```bash cURL
288
444
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,variants' \
289
445
  -H 'X-Spree-API-Key: pk_xxx'
@@ -322,7 +478,7 @@ For example:
322
478
  Products can belong to multiple categories.
323
479
 
324
480
 
325
- ```typescript SDK
481
+ ```typescript Store SDK
326
482
  // List categories
327
483
  const { data: categories } = await client.categories.list()
328
484
 
@@ -335,6 +491,10 @@ const { data: products } = await client.categories.products.list('clothing/shirt
335
491
  })
336
492
  ```
337
493
 
494
+ ```typescript Admin SDK
495
+ const { data: categories } = await adminClient.categories.list()
496
+ ```
497
+
338
498
  ```bash cURL
339
499
  # List categories
340
500
  curl 'https://api.mystore.com/api/v3/store/categories' \
@@ -371,7 +531,7 @@ Product `status` (`draft` / `active` / `archived`) is the **outer gate**: a Draf
371
531
  Publications appear in the API under `product_publications` when expanded; the same data is available through the `channels` association as a flat list of joined channels.
372
532
 
373
533
 
374
- ```typescript SDK
534
+ ```typescript Admin SDK
375
535
  const product = await adminClient.products.get('prod_abc', {
376
536
  expand: ['product_publications', 'channels'],
377
537
  })
@@ -411,7 +571,7 @@ Two write surfaces serve different shapes:
411
571
 
412
572
 
413
573
 
414
- ```typescript SDK
574
+ ```typescript Admin SDK
415
575
  await adminClient.products.update('prod_abc', {
416
576
  product_publications: [
417
577
  { channel_id: 'ch_online' },
@@ -438,7 +598,7 @@ Two write surfaces serve different shapes:
438
598
 
439
599
 
440
600
 
441
- ```typescript SDK
601
+ ```typescript Admin SDK
442
602
  await adminClient.channels.addProducts('ch_online', {
443
603
  product_ids: ['prod_abc', 'prod_def'],
444
604
  published_at: '2026-07-01T00:00:00Z',
@@ -473,7 +633,7 @@ The two surfaces converge on the same `spree_product_publications` table — pic
473
633
  Storefronts and `client.products.list()` calls return only products published on the resolved channel (live within the publication window, with the product itself `active`). To scope a Store SDK request to a non-default channel — e.g. a POS app querying for the POS catalog — set the channel `code` on the client or per-request:
474
634
 
475
635
 
476
- ```typescript SDK
636
+ ```typescript Store SDK
477
637
  // Client-level default
478
638
  const client = createClient({ baseUrl, publishableKey, channel: 'pos' })
479
639
 
@@ -481,6 +641,11 @@ const client = createClient({ baseUrl, publishableKey, channel: 'pos' })
481
641
  const posProducts = await client.products.list({}, { channel: 'pos' })
482
642
  ```
483
643
 
644
+ ```typescript Admin SDK
645
+ // filter by the channel's code via Ransack (q[channels_code_eq])
646
+ const { data: products } = await adminClient.products.list({ channels_code_eq: 'pos' })
647
+ ```
648
+
484
649
  ```bash cURL
485
650
  curl 'https://api.mystore.com/api/v3/store/products' \
486
651
  -H 'X-Spree-API-Key: pk_xxx' \
@@ -504,4 +669,5 @@ See [Sales Channels](channels.md) for the full channel lifecycle, including defa
504
669
  - [Media](media.md) — Image management
505
670
  - [Translations](translations.md) — Translating product content
506
671
  - [Search & Filtering](search-filtering.md) — Full-text search and Ransack filtering
672
+ - [Store SDK Products](../sdk/store/products.md) — Listing, fetching, filtering, and categories via `client.products`
507
673
  - [Querying](../../api-reference/store-api/querying.md) — API filtering, sorting, and pagination