@spree/docs 0.1.7 → 0.1.9

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.
@@ -238,23 +238,23 @@ Apply or remove promotional coupon codes during checkout:
238
238
 
239
239
 
240
240
  ```typescript SDK
241
- // Apply a coupon
242
- await client.carts.couponCodes.apply(cartId, 'SAVE20')
241
+ // Apply a discount code
242
+ await client.carts.discountCodes.apply(cartId, 'SAVE20')
243
243
 
244
- // Remove a coupon
245
- await client.carts.couponCodes.remove(cartId, 'SAVE20')
244
+ // Remove a discount code
245
+ await client.carts.discountCodes.remove(cartId, 'SAVE20')
246
246
  ```
247
247
 
248
248
  ```bash cURL
249
- # Apply a coupon
250
- curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/coupon_codes' \
249
+ # Apply a discount code
250
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/discount_codes' \
251
251
  -H 'Authorization: Bearer spree_pk_xxx' \
252
252
  -H 'X-Spree-Token: abc123' \
253
253
  -H 'Content-Type: application/json' \
254
254
  -d '{ "code": "SAVE20" }'
255
255
 
256
- # Remove a coupon
257
- curl -X DELETE 'https://api.mystore.com/api/v3/store/carts/cart_xxx/coupon_codes/SAVE20' \
256
+ # Remove a discount code
257
+ curl -X DELETE 'https://api.mystore.com/api/v3/store/carts/cart_xxx/discount_codes/SAVE20' \
258
258
  -H 'Authorization: Bearer spree_pk_xxx' \
259
259
  -H 'X-Spree-Token: abc123'
260
260
  ```
@@ -278,13 +278,13 @@ Customers apply coupon codes during checkout via the Store API:
278
278
 
279
279
 
280
280
  ```typescript SDK
281
- // Apply a coupon code to the cart
282
- const cart = await client.carts.couponCodes.apply('cart_abc123', 'SUMMER20', {
281
+ // Apply a discount code to the cart
282
+ const cart = await client.carts.discountCodes.apply('cart_abc123', 'SUMMER20', {
283
283
  spreeToken: '<token>',
284
284
  })
285
285
 
286
- // Remove a coupon code from the cart
287
- await client.carts.couponCodes.remove('cart_abc123', 'SUMMER20', {
286
+ // Remove a discount code from the cart
287
+ await client.carts.discountCodes.remove('cart_abc123', 'SUMMER20', {
288
288
  spreeToken: '<token>',
289
289
  })
290
290
  ```
@@ -73,9 +73,14 @@ const { data: products } = await client.categories.products.list('clothing/shirt
73
73
  limit: 12,
74
74
  })
75
75
 
76
- // Or filter by category ID
76
+ // Or filter by category ID (includes descendants)
77
77
  const { data: products } = await client.products.list({
78
- categories_id_eq: 'ctg_xxx',
78
+ in_category: 'ctg_xxx',
79
+ })
80
+
81
+ // Multiple categories (OR logic, checkbox filters)
82
+ const { data: products } = await client.products.list({
83
+ in_categories: ['ctg_shirts', 'ctg_pants'],
79
84
  })
80
85
  ```
81
86
 
@@ -84,8 +89,12 @@ const { data: products } = await client.products.list({
84
89
  curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts/products?limit=12' \
85
90
  -H 'Authorization: Bearer spree_pk_xxx'
86
91
 
87
- # Filter by category ID
88
- curl 'https://api.mystore.com/api/v3/store/products?q[categories_id_eq]=ctg_xxx' \
92
+ # Filter by category ID (includes descendants)
93
+ curl 'https://api.mystore.com/api/v3/store/products?q[in_category]=ctg_xxx' \
94
+ -H 'Authorization: Bearer spree_pk_xxx'
95
+
96
+ # Multiple categories (OR logic)
97
+ curl 'https://api.mystore.com/api/v3/store/products?q[in_categories][]=ctg_shirts&q[in_categories][]=ctg_pants' \
89
98
  -H 'Authorization: Bearer spree_pk_xxx'
90
99
  ```
91
100
 
@@ -248,15 +248,21 @@ Gift cards are applied using the same coupon codes endpoint as promotion codes.
248
248
 
249
249
  ```typescript SDK
250
250
  // Apply gift card code to cart (works for guests and registered customers)
251
- const cart = await client.carts.couponCodes.apply('cart_abc123', 'abc1234def', {
251
+ // Gift cards use a dedicated endpoint — they reduce amount_due, not total
252
+ const cart = await client.carts.giftCards.apply('cart_abc123', 'abc1234def', {
253
+ spreeToken: '<token>',
254
+ })
255
+
256
+ // Remove gift card (ID from cart.gift_card.id)
257
+ await client.carts.giftCards.remove('cart_abc123', cart.gift_card.id, {
252
258
  spreeToken: '<token>',
253
259
  })
254
260
  ```
255
261
 
256
262
  ```bash cURL
257
263
  # Apply gift card to cart (works for guests and registered customers)
258
- curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_abc123/coupon_codes' \
259
- -H 'Authorization: Bearer spree_pk_xxx' \
264
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_abc123/gift_cards' \
265
+ -H 'X-Spree-Api-Key: pk_xxx' \
260
266
  -H 'X-Spree-Token: <token>' \
261
267
  -H 'Content-Type: application/json' \
262
268
  -d '{ "code": "abc1234def" }'
@@ -163,22 +163,56 @@ Each webhook request includes these headers:
163
163
 
164
164
  ### Verifying Webhook Signatures
165
165
 
166
- To ensure webhooks are genuinely from your Spree store, verify the signature:
166
+ To ensure webhooks are genuinely from your Spree store, verify the signature.
167
+
168
+ #### Next.js (recommended)
169
+
170
+ Use `@spree/next/webhooks` for a ready-made Route Handler:
171
+
172
+ ```typescript
173
+ // src/app/api/webhooks/spree/route.ts
174
+ import { createWebhookHandler } from '@spree/next/webhooks'
175
+
176
+ export const POST = createWebhookHandler({
177
+ secret: process.env.SPREE_WEBHOOK_SECRET!,
178
+ handlers: {
179
+ 'order.completed': async (event) => {
180
+ // event.data is the order payload (same shape as Store API)
181
+ await sendOrderConfirmationEmail(event.data)
182
+ },
183
+ 'order.canceled': async (event) => {
184
+ await sendCancellationEmail(event.data)
185
+ },
186
+ },
187
+ })
188
+ ```
167
189
 
168
- ```ruby
169
- # In your webhook receiver
170
- def verify_webhook(request)
171
- payload = request.body.read
172
- signature = request.headers['X-Spree-Webhook-Signature']
173
- secret_key = ENV['SPREE_WEBHOOK_SECRET'] # Your endpoint's secret_key
190
+ #### Any JavaScript/TypeScript framework
174
191
 
175
- expected = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
192
+ Use `@spree/sdk/webhooks` for framework-agnostic verification:
176
193
 
177
- ActiveSupport::SecurityUtils.secure_compare(signature, expected)
178
- end
194
+ ```typescript
195
+ import { verifyWebhookSignature } from '@spree/sdk/webhooks'
196
+ import type { WebhookEvent } from '@spree/sdk/webhooks'
197
+ import type { Order } from '@spree/sdk'
198
+
199
+ // Hono, Cloudflare Workers, or any Web Fetch API-based framework
200
+ app.post('/webhooks/spree', async (req, res) => {
201
+ const body = await req.text()
202
+ const signature = req.headers['x-spree-webhook-signature']
203
+ const timestamp = req.headers['x-spree-webhook-timestamp']
204
+
205
+ if (!verifyWebhookSignature(body, signature, timestamp, process.env.SPREE_WEBHOOK_SECRET!)) {
206
+ return res.status(401).json({ error: 'Invalid signature' })
207
+ }
208
+
209
+ const event: WebhookEvent<Order> = JSON.parse(body)
210
+ // handle event...
211
+ res.json({ received: true })
212
+ })
179
213
  ```
180
214
 
181
- Example in a Rails controller:
215
+ #### Ruby
182
216
 
183
217
  ```ruby
184
218
  class WebhooksController < ApplicationController
@@ -209,7 +243,8 @@ class WebhooksController < ApplicationController
209
243
  request.body.rewind
210
244
 
211
245
  signature = request.headers['X-Spree-Webhook-Signature']
212
- expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['SPREE_WEBHOOK_SECRET'], payload)
246
+ timestamp = request.headers['X-Spree-Webhook-Timestamp']
247
+ expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['SPREE_WEBHOOK_SECRET'], "#{timestamp}.#{payload}")
213
248
 
214
249
  ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected)
215
250
  end
@@ -61,13 +61,15 @@ The SDK uses a resource builder pattern for nested resources:
61
61
  | `carts` | `paymentMethods` | `list` |
62
62
  | `carts` | `paymentSessions` | `create`, `get`, `update`, `complete` |
63
63
  | `carts` | `fulfillments` | `list`, `update` |
64
- | `carts` | `couponCodes` | `apply`, `remove` |
64
+ | `carts` | `discountCodes` | `apply`, `remove` |
65
+ | `carts` | `giftCards` | `apply`, `remove` |
65
66
  | `carts` | `storeCredits` | `apply`, `remove` |
66
67
  | `customer` | `addresses` | `list`, `get`, `create`, `update`, `delete`, `markAsDefault` |
67
68
  | `customer` | `creditCards` | `list`, `get`, `delete` |
68
69
  | `customer` | `giftCards` | `list`, `get` |
69
70
  | `customer` | `orders` | `list` |
70
71
  | `customer` | `paymentSetupSessions` | `create`, `get`, `complete` |
72
+ | `policies` | — | `list`, `get` |
71
73
  | `categories` | `products` | `list` |
72
74
  | `wishlists` | `items` | `create`, `update`, `delete` |
73
75
 
@@ -55,16 +55,26 @@ await client.carts.items.update(cartId, lineItemId, {
55
55
  await client.carts.items.delete(cartId, lineItemId, options);
56
56
  ```
57
57
 
58
- ### Coupon Codes
58
+ ### Discount Codes
59
59
 
60
60
  ```typescript
61
61
  const options = { spreeToken: cart.token };
62
62
 
63
- // Apply a coupon code
64
- await client.carts.couponCodes.apply(cartId, 'SAVE20', options);
63
+ // Apply a discount code
64
+ await client.carts.discountCodes.apply(cartId, 'SAVE20', options);
65
65
 
66
- // Remove a coupon code
67
- await client.carts.couponCodes.remove(cartId, 'SUMMER20', options);
66
+ // Remove a discount code
67
+ await client.carts.discountCodes.remove(cartId, 'SAVE20', options);
68
+ ```
69
+
70
+ ### Gift Cards
71
+
72
+ ```typescript
73
+ // Apply a gift card (reduces amount_due, not total)
74
+ const cart = await client.carts.giftCards.apply(cartId, 'GC-ABCD-1234', options);
75
+
76
+ // Remove a gift card (ID from cart.gift_card.id)
77
+ await client.carts.giftCards.remove(cartId, 'gc_abc123', options);
68
78
  ```
69
79
 
70
80
  ## Checkout
@@ -20,6 +20,9 @@ Set environment variables and the client initializes automatically:
20
20
  ```env
21
21
  SPREE_API_URL=https://api.mystore.com
22
22
  SPREE_PUBLISHABLE_KEY=spree_pk_xxx
23
+
24
+ # Required for webhook-based emails (see Webhooks section below)
25
+ SPREE_WEBHOOK_SECRET=your_webhook_endpoint_secret_key
23
26
  ```
24
27
 
25
28
  ### Explicit initialization
@@ -120,19 +123,21 @@ Checkout functions use the implicit cart resolved from cookies. No `orderId` is
120
123
  import {
121
124
  getCheckout,
122
125
  updateCheckout,
123
- getFulfillments,
124
126
  selectDeliveryRate,
125
- applyCoupon,
126
- removeCoupon,
127
+ applyDiscountCode,
128
+ removeDiscountCode,
129
+ applyGiftCard,
130
+ removeGiftCard,
127
131
  complete,
128
132
  } from '@spree/next';
129
133
 
130
134
  const checkout = await getCheckout();
131
135
  await updateCheckout({ shipping_address: { ... }, billing_address: { ... } });
132
- const fulfillments = await getFulfillments();
133
136
  await selectDeliveryRate(fulfillmentId, deliveryRateId);
134
- await applyCoupon('SAVE20');
135
- await removeCoupon(promotionId);
137
+ await applyDiscountCode('SAVE20');
138
+ await removeDiscountCode('SAVE20');
139
+ await applyGiftCard('GC-ABCD-1234');
140
+ await removeGiftCard('gc_abc123');
136
141
  await complete();
137
142
  ```
138
143
 
@@ -282,6 +287,48 @@ const products = await listProducts({ limit: 10 }, { locale: 'fr', country: 'FR'
282
287
  const category = await getCategory('clothing/shirts', {}, { locale: 'de', country: 'DE' });
283
288
  ```
284
289
 
290
+ ## Webhooks
291
+
292
+ Handle Spree webhook events in your Next.js app with `@spree/next/webhooks`:
293
+
294
+ ```typescript
295
+ // src/app/api/webhooks/spree/route.ts
296
+ import { createWebhookHandler } from '@spree/next/webhooks'
297
+
298
+ export const POST = createWebhookHandler({
299
+ secret: process.env.SPREE_WEBHOOK_SECRET!,
300
+ handlers: {
301
+ 'order.completed': async (event) => {
302
+ await sendOrderConfirmationEmail(event.data)
303
+ },
304
+ 'order.canceled': async (event) => {
305
+ await sendCancellationEmail(event.data)
306
+ },
307
+ 'order.shipped': async (event) => {
308
+ await sendShippingNotification(event.data)
309
+ },
310
+ 'customer.password_reset_requested': async (event) => {
311
+ await sendPasswordResetEmail(event.data)
312
+ },
313
+ },
314
+ })
315
+ ```
316
+
317
+ The handler verifies HMAC-SHA256 signatures, rejects replayed requests, routes events to your handlers, and returns 200 immediately (handlers run async).
318
+
319
+ ### Local development
320
+
321
+ Webhooks require a publicly accessible URL. Use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/) for local development:
322
+
323
+ ```bash
324
+ brew install cloudflared
325
+ cloudflared tunnel --url http://localhost:3001
326
+ ```
327
+
328
+ Then create a webhook endpoint in **Spree Admin → Settings → Developers → Webhooks** pointing to your tunnel URL + `/api/webhooks/spree`.
329
+
330
+ For more details, see [Webhooks](../../core-concepts/webhooks.md).
331
+
285
332
  ## TypeScript
286
333
 
287
334
  All types are re-exported from `@spree/sdk`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",