@spree/docs 0.1.241 → 0.1.242

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.
@@ -155,7 +155,8 @@ otherwise every parcel is quoted light.
155
155
 
156
156
  Package types are that vocabulary: the box parcels ship in, and for wholesale,
157
157
  the cartons products are packed into and the pallets or containers an order
158
- leaves on.
158
+ leaves on. How those cartons and pallets roll up into a quote is
159
+ [freight](freight.md).
159
160
 
160
161
 
161
162
  ```typescript Admin SDK
@@ -248,6 +249,7 @@ box of their own with all three dimensions and a weight recorded. See
248
249
 
249
250
  ## Related
250
251
 
252
+ - [Freight](freight.md) — cartons, pallets and wholesale shipping
251
253
  - [Fulfillments](fulfillments.md) — what happens to the parcel afterwards
252
254
  - [Calculators](calculators.md) — how a price is worked out
253
255
  - [EasyPost](../../integrations/shipping/easypost.md) — live carrier rates and labels
@@ -0,0 +1,279 @@
1
+ ---
2
+ title: Freight
3
+ description: How wholesale orders ship — the carton and pallet chain a product declares, the logistics rollup a forwarder quotes against, and rates that carry no price yet.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ A wholesale order does not leave the warehouse as parcels. It leaves as
9
+ cartons, a pallet, or a container, and for international freight the price is
10
+ quoted by a forwarder after someone looks at the order — not by an API at
11
+ checkout.
12
+
13
+ Four pieces make that work, and none of them disturb retail shipping:
14
+
15
+ | Piece | Question it answers |
16
+ |---|---|
17
+ | **Carton details** on a product | How do these units pack? |
18
+ | **Freight summary** on a cart or order | How big is the whole load? |
19
+ | **Volume and company rules** on a delivery method | Which shipment tier is this? |
20
+ | **Unpriced rates** | What does a buyer see before a price exists? |
21
+
22
+ One catalog serves both audiences. The same product ships parcel to a shopper
23
+ and carton to a trade buyer, because freight methods sit on the ordinary
24
+ shipping profile and are gated by who is buying rather than by what is bought.
25
+ See [delivery setup](delivery-setup.md) for profiles,
26
+ zones and methods, and [package types](delivery-setup.md#packaging)
27
+ for the packaging vocabulary this builds on.
28
+
29
+ ## How products pack
30
+
31
+ Freight numbers are not derivable from retail shipping data. A case of 48
32
+ bottles is one carton, not 48 parcels — so a product says how its units pack,
33
+ and the rest follows the chain **unit → carton → pallet → cubic meters and
34
+ weight**.
35
+
36
+ The geometry lives on a shared carton, because merchants reuse a handful of
37
+ standard sizes across hundreds of products and one edit should fix all of
38
+ them. What varies per product stays on the product:
39
+
40
+ | Field | What it says |
41
+ |---|---|
42
+ | `carton_package_type_id` | Which carton this is packed into. Must be a package type of kind `carton` |
43
+ | `units_per_carton` | How many units one carton holds |
44
+ | `carton_weight` | What a packed carton weighs, gross. Optional |
45
+ | `cartons_per_pallet` | How those cartons stack |
46
+ | `purchase_unit` | `unit` or `carton` — what a buyer is quoted in |
47
+
48
+
49
+ ```typescript Admin SDK
50
+ // The carton, once, shared by every product packed in it.
51
+ const carton = await adminClient.packageTypes.create({
52
+ name: 'Master carton',
53
+ kind: 'carton',
54
+ length: 40, width: 30, height: 25, dimensions_unit: 'cm',
55
+ weight: 0.4, max_weight: 20, weight_unit: 'kg',
56
+ })
57
+
58
+ // How this product fills it.
59
+ await adminClient.products.variants.update('prod_xxx', 'variant_xxx', {
60
+ carton_package_type_id: carton.id,
61
+ units_per_carton: 48,
62
+ carton_weight: 19.2,
63
+ cartons_per_pallet: 40,
64
+ purchase_unit: 'carton',
65
+ })
66
+ ```
67
+
68
+ ```bash cURL
69
+ curl -X POST 'https://api.mystore.com/api/v3/admin/package_types' \
70
+ -H 'X-Spree-API-Key: sk_xxx' \
71
+ -H 'Content-Type: application/json' \
72
+ -d '{
73
+ "name": "Master carton", "kind": "carton",
74
+ "length": 40, "width": 30, "height": 25, "dimensions_unit": "cm",
75
+ "weight": 0.4, "max_weight": 20, "weight_unit": "kg"
76
+ }'
77
+
78
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx/variants/variant_xxx' \
79
+ -H 'X-Spree-API-Key: sk_xxx' \
80
+ -H 'Content-Type: application/json' \
81
+ -d '{
82
+ "carton_package_type_id": "pkgtype_xxx",
83
+ "units_per_carton": 48,
84
+ "carton_weight": 19.2,
85
+ "cartons_per_pallet": 40,
86
+ "purchase_unit": "carton"
87
+ }'
88
+ ```
89
+
90
+
91
+ Stored quantities are always units, at every level. `purchase_unit` only
92
+ changes the vocabulary a storefront presents — a buyer shown "2 cartons" is
93
+ buying 96 units.
94
+
95
+ Two data errors are refused while the merchant is still editing rather than
96
+ discovered through a customer complaint: an `order_multiple` that straddles
97
+ carton boundaries can never ship whole, and quoting in cartons without saying
98
+ how many units one holds leaves a storefront no way to render the offer.
99
+
100
+ Reading a product back adds `units_per_pallet`, derived from the two divisors
101
+ so a merchant sees the far end of the chain without doing the arithmetic. It
102
+ is null unless both halves are recorded.
103
+
104
+ > **NOTE:** A product may only be packed into a package type of kind `carton`, and a
105
+ > carton with products packed into it cannot later become a pallet. Repack the
106
+ > products first.
107
+
108
+ ## The freight summary
109
+
110
+ The **freight summary** is the load as a forwarder reads it: how many units,
111
+ the cartons they fill, the pallets those stack onto, the cubic meters they
112
+ occupy and what the whole thing weighs.
113
+
114
+ ```json
115
+ {
116
+ "total_units": 480,
117
+ "total_cartons": 10,
118
+ "total_pallets": 1,
119
+ "total_volume": "0.3",
120
+ "total_weight": "192.0",
121
+ "complete": true
122
+ }
123
+ ```
124
+
125
+ `total_volume` is cubic meters and `total_weight` is kilograms, both as
126
+ decimal strings. `total_pallets` is null unless every carton-bearing line says
127
+ how it stacks — one silent product would understate the load, so no figure is
128
+ better than a wrong one.
129
+
130
+ **`complete` is the field to read before quoting from any of it.** False means
131
+ part of the catalog carries no carton data, so those products were measured
132
+ loose from their unit dimensions instead. The numbers are still the best
133
+ available and real enough to quote against, but they are a floor rather than a
134
+ total, and a surface using them should say so.
135
+
136
+ A cart of unmeasured goods has no freight summary at all rather than one full
137
+ of zeros, which would read as a shipment that takes up no space.
138
+
139
+ ### Where it appears
140
+
141
+ | Response | Shape |
142
+ |---|---|
143
+ | Store cart | Totals only |
144
+ | Store delivery rate | Totals only, on freight rates |
145
+ | Admin cart and order | Totals plus a `lines` breakdown |
146
+ | Admin delivery rate | Totals plus `lines` |
147
+
148
+ The per-product breakdown is back-office only. A buyer is told what their
149
+ shipment costs and how big it is; which SKU accounts for which part of it is
150
+ the warehouse's business. Each admin line carries `variant_id`, `sku`, `name`,
151
+ `units`, `cartons`, `pallets`, `units_per_carton`, `cartons_per_pallet`,
152
+ `weight_per_carton`, `volume`, `weight` and its own `complete` flag.
153
+
154
+
155
+ ```typescript SDK
156
+ const cart = await client.carts.get('cart_xxx')
157
+
158
+ if (cart.freight_summary) {
159
+ const { total_cartons, total_pallets, total_volume, complete } = cart.freight_summary
160
+ // `complete: false` — show these as a minimum, not a total.
161
+ }
162
+ ```
163
+
164
+ ```bash cURL
165
+ curl 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
166
+ -H 'X-Spree-API-Key: pk_xxx' \
167
+ -H 'X-Spree-Token: guest_token'
168
+ ```
169
+
170
+
171
+ > **NOTE:** A placed order's summary is never re-derived from the live catalog. The freight
172
+ > provider freezes a copy onto the rate it quoted, and the order reads that — so
173
+ > a carton size corrected next month does not rewrite what this container held.
174
+ > Two freight consignments on one order are merged into the one load the
175
+ > forwarder sees, and a canceled one is left out.
176
+
177
+ ## Freight rates
178
+
179
+ A freight method is an ordinary [delivery method](delivery-setup.md#delivery-methods-and-zones)
180
+ priced by the **Freight** rate provider. That provider quotes nothing and says
181
+ so: it returns a rate with no price, carrying the freight summary the merchant
182
+ will send to the forwarder in place of an amount.
183
+
184
+ Such a rate comes back with `unpriced: true`, and every money field on it reads
185
+ **"Quoted after review"** rather than a figure. That matters: a zero cost
186
+ rendering as "Free" over a container of goods is a promise the merchant cannot
187
+ keep. Unpriced rates are also sorted after every priced one and are never
188
+ preselected, so a buyer with a real parcel option still sees it first.
189
+
190
+
191
+ ```typescript SDK
192
+ const rates = fulfillment.delivery_rates
193
+
194
+ rates.forEach((rate) => {
195
+ if (rate.unpriced) {
196
+ rate.display_total // "Quoted after review"
197
+ rate.freight_summary // what the forwarder will quote against
198
+ } else {
199
+ rate.display_total // "$24.00"
200
+ }
201
+ })
202
+ ```
203
+
204
+ ```bash cURL
205
+ curl 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
206
+ -H 'X-Spree-API-Key: pk_xxx' \
207
+ -H 'X-Spree-Token: guest_token'
208
+ ```
209
+
210
+
211
+ Checkout completes on an unpriced rate. The shipping price arrives afterwards,
212
+ once the forwarder has quoted it.
213
+
214
+ > **NOTE:** How a wholesale buyer **pays** — deposits, net terms, credit limits — is a
215
+ > separate subject and is not part of freight in 6.0. Partial payments are
216
+ > generally available and predate this work. See
217
+ > [payments](payments.md).
218
+
219
+ ## Shipment tiers
220
+
221
+ Which freight method a load qualifies for is configuration, not code. Each
222
+ tier — "Cartons", "Pallet", "20ft container" — is an ordinary delivery method
223
+ bounded by a rule.
224
+
225
+ **Volume rule.** Bounds the method by packed volume in cubic meters: a pallet
226
+ method takes 1–15 CBM, a 20ft container 15–33, and the estimator offers
227
+ whichever one the order actually fills.
228
+
229
+ Its minimum is deliberately forgiving. A partly measured catalog understates
230
+ the load, so the figure is a floor rather than a measurement — it may raise a
231
+ shipment into a tier but never exclude it from one. A maximum still applies,
232
+ since passing one on too small a number errs toward offering the method.
233
+
234
+ **Company rule.** Splits freight from parcel by who is buying. On, the method
235
+ is offered only to orders placed for a [company](companies.md),
236
+ so a carton tier disappears from retail carts. Off, only to orders that are
237
+ not, which keeps parcel methods away from wholesale buyers. A method with no
238
+ company rule is offered to both, so the rule is only ever added to state a
239
+ split.
240
+
241
+
242
+ ```typescript Admin SDK
243
+ await adminClient.deliveryMethods.create({
244
+ name: 'Pallet freight',
245
+ rate_provider: 'Spree::DeliveryRateProvider::Freight',
246
+ rules: [
247
+ { type: 'volume_rule', preferences: { minimum_volume: 1, maximum_volume: 15 } },
248
+ { type: 'company_rule', preferences: { company_orders_only: true } },
249
+ ],
250
+ })
251
+ ```
252
+
253
+ ```bash cURL
254
+ curl -X POST 'https://api.mystore.com/api/v3/admin/delivery_methods' \
255
+ -H 'X-Spree-API-Key: sk_xxx' \
256
+ -H 'Content-Type: application/json' \
257
+ -d '{
258
+ "name": "Pallet freight",
259
+ "rate_provider": "Spree::DeliveryRateProvider::Freight",
260
+ "rules": [
261
+ { "type": "volume_rule", "preferences": { "minimum_volume": 1, "maximum_volume": 15 } },
262
+ { "type": "company_rule", "preferences": { "company_orders_only": true } }
263
+ ]
264
+ }'
265
+ ```
266
+
267
+
268
+ A freight method never consults a calculator — there is no price to work out —
269
+ so admin surfaces hide its pricing form. `GET /api/v3/admin/delivery_methods/rate_providers`
270
+ lists the providers installed on the store, which is what a picker should read
271
+ rather than a hardcoded list.
272
+
273
+ ## Related
274
+
275
+ - [Delivery setup](delivery-setup.md) — profiles, zones, methods and package types
276
+ - [Fulfillments](fulfillments.md) — what happens to the consignment afterwards
277
+ - [Companies](companies.md) — who a wholesale order is placed for
278
+ - [Build a B2B store](../how-to/build-a-b2b-store.md) — the wholesale setup end to end
279
+ - [Package types](/user/settings/package-types) — configuring packaging in the dashboard
@@ -11,7 +11,7 @@ Payment methods typically represent a payment gateway. Gateways will process car
11
11
 
12
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
- 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.
14
+ A payment's `number` is derived from what it belongs to — an order `R1001` numbers its payments `R1001-P1`, `R1001-P2` — so a gateway reference traces straight back to the order without a lookup.
15
15
 
16
16
  ### Payment Architecture Diagram
17
17
 
@@ -20,9 +20,8 @@ erDiagram
20
20
  Payment {
21
21
  string number
22
22
  decimal amount
23
- string state
23
+ string status
24
24
  string response_code
25
- string avs_response
26
25
  string source_type
27
26
  string source_id
28
27
  }
@@ -32,7 +31,7 @@ erDiagram
32
31
  string type
33
32
  string description
34
33
  boolean active
35
- string display_on
34
+ boolean storefront_visible
36
35
  integer position
37
36
  }
38
37
 
@@ -58,11 +57,12 @@ erDiagram
58
57
  }
59
58
 
60
59
  CreditCard {
61
- string last_digits
62
- string cc_type
60
+ string last4
61
+ string brand
63
62
  integer month
64
63
  integer year
65
64
  string name
65
+ boolean default
66
66
  }
67
67
 
68
68
  GatewayCustomer {
@@ -124,12 +124,12 @@ A `PaymentMethod` can have the following attributes:
124
124
  | `name` | The visible name for this payment method | `Check` |
125
125
  | `description`| The description for this payment method | `Pay by check.` |
126
126
  | `active` | Whether or not this payment method is active. Set it `false` to hide it in the Store API. | `true` |
127
- | `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` |
127
+ | `storefront_visible` | Whether customers can choose this method. Leave it off for a method only staff use. | `true` |
128
128
  | `position` | The position of the payment method in lists. Lower numbers appear first. | `1` |
129
129
 
130
130
  > **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.
131
131
 
132
- ### Session-based vs Legacy Payment Methods
132
+ ### Session-based vs direct payment methods
133
133
 
134
134
  Payment methods indicate whether they use the modern session-based flow via the `session_required?` method:
135
135
 
@@ -140,13 +140,13 @@ Payment methods indicate whether they use the modern session-based flow via the
140
140
  | `payment_session_class` | Returns the STI subclass of `Spree::PaymentSession` for this gateway (e.g., `Spree::PaymentSessions::Stripe`). | `nil` |
141
141
  | `payment_setup_session_class` | Returns the STI subclass of `Spree::PaymentSetupSession` for this gateway. | `nil` |
142
142
 
143
- Modern gateways like Stripe and Adyen set `session_required?` to `true`. The Store API serializer includes this as the `session_required` field so your frontend knows which flow to use.
143
+ Gateways like Stripe and Adyen set `session_required?` to `true`; offline methods leave it `false`. Neither path is deprecated. The Store API serializer includes this as the `session_required` field so your frontend knows which flow to use.
144
144
 
145
- ### Non-Session Payment Methods (Manual/Offline)
145
+ ### Direct payment methods (manual and offline)
146
146
 
147
147
  Payment methods where `session_required?` returns `false` don't need a payment session. These are typically offline or manual payment methods such as:
148
148
 
149
- - **Check** — built-in (`Spree::PaymentMethod::Check`)
149
+ - **Check** — built in
150
150
  - **Cash on Delivery** — customer pays upon delivery
151
151
  - **Bank Transfer / Wire** — customer transfers money to a bank account
152
152
  - **Purchase Order** — common in B2B, customer provides a PO number
@@ -180,9 +180,16 @@ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payments' \
180
180
  ```
181
181
 
182
182
 
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.
183
+ The payment starts at `checkout`. Completing the cart processes it — for a manual method there is no provider to call, so it succeeds immediately and lands on `pending`, or `completed` when the store charges at checkout.
184
+
185
+ Once the money actually arrives — the cheque clears, the transfer lands, the
186
+ driver is paid — staff capture the payment from the dashboard, or you do it
187
+ through the Admin API:
184
188
 
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).
189
+ ```typescript Admin SDK
190
+ await adminClient.orders.payments.capture(orderId, paymentId)
191
+ await adminClient.orders.payments.void(orderId, paymentId)
192
+ ```
186
193
 
187
194
  ## Payment Flow
188
195
 
@@ -247,7 +254,7 @@ After the customer completes payment, the frontend calls the Complete Payment Se
247
254
 
248
255
  **Step 4: Complete Order**
249
256
 
250
- The frontend calls `POST /carts/:id/complete` to finalize the order. Spree validates the order is ready (addresses, fulfillments, payment), advances through any remaining checkout states, and marks the order as complete.
257
+ The frontend calls `POST /carts/:id/complete` to finalize the order. Spree checks the cart has everything it needs — addresses, fulfillments, payment — and turns it into an order.
251
258
 
252
259
  This separation ensures the same flow works for all payment types — inline cards, offsite redirects, and wallet payments.
253
260
 
@@ -305,11 +312,11 @@ sequenceDiagram
305
312
  Spree API-->>Frontend: Cart (with embedded payment_methods, each carrying session_required)
306
313
 
307
314
  Frontend->>Spree API: POST /payments (payment_method_id)
308
- Spree API->>Spree API: Create Payment (checkout state)
315
+ Spree API->>Spree API: Create payment (status checkout)
309
316
  Spree API-->>Frontend: Payment created
310
317
 
311
318
  Frontend->>Spree API: POST /carts/:id/complete
312
- Spree API->>Spree API: process_payments! (authorize succeeds immediately)
319
+ Spree API->>Spree API: Process payment (succeeds immediately)
313
320
  Spree API->>Spree API: Payment → pending/completed
314
321
  Spree API-->>Frontend: Order completed
315
322
  ```
@@ -320,16 +327,18 @@ The frontend reads the cart's embedded `payment_methods` (returned by `GET /cart
320
327
 
321
328
  **Step 2: Create payment**
322
329
 
323
- The frontend calls `POST /payments` with the `payment_method_id`. Spree creates a `Payment` record in `checkout` state. No external provider interaction is needed.
330
+ The frontend calls `POST /payments` with the `payment_method_id`. Spree creates a payment with status `checkout`. No provider is contacted.
331
+
332
+ Two refusals to handle: a method needing a session returns `payment_session_required`, and one unavailable for this order returns `payment_method_unavailable` — both HTTP 422 with that `code`.
324
333
 
325
334
  **Step 3: Complete order**
326
335
 
327
- The frontend completes the order. Spree's `process_payments!` runs the payment method's `authorize` (or `purchase` with auto-capture). For manual methods like Check, these return an immediate success — no external service is called. The payment transitions to `pending` (without auto-capture) or `completed` (with auto-capture) and the order completes. The merchant can later capture `pending` payments from the Admin Panel once the physical payment is received.
336
+ The frontend completes the order. Completing the cart processes the payment. For a manual method there is nothing to call, so it succeeds immediately and lands on `pending`, or `completed` when the store charges at checkout. Staff capture `pending` payments from the dashboard once the money arrives.
328
337
 
329
338
 
330
339
  ### Payment Session
331
340
 
332
- A `PaymentSession` (`Spree::PaymentSession`) represents a server-side session with the payment gateway. It is the entry point for every payment attempt and holds the provider-specific data needed by the frontend SDK.
341
+ A payment session represents a server-side session with the payment gateway. It is the entry point for every payment attempt and holds the provider-specific data needed by the frontend SDK.
333
342
 
334
343
  #### Attributes
335
344
 
@@ -342,6 +351,11 @@ A `PaymentSession` (`Spree::PaymentSession`) represents a server-side session wi
342
351
  | `external_data` | Provider-specific data including `client_secret` for frontend SDK | `{"client_secret": "pi_3ABC_secret_xyz"}` |
343
352
  | `customer_external_id` | The provider's customer ID | `cus_ABC123` |
344
353
  | `expires_at` | When the session expires | `2025-01-01T12:00:00Z` |
354
+ | `cart_id` | The cart being paid for | `cart_k5nR8xLq` |
355
+
356
+ > **NOTE:** A session also carries `order_id`, but during checkout there is no order yet —
357
+ > it reports the cart's ID until the cart completes. Read `cart_id` while paying,
358
+ > and `order_id` once you have an order.
345
359
 
346
360
  #### States
347
361
 
@@ -359,6 +373,10 @@ stateDiagram-v2
359
373
  processing --> expired
360
374
  ```
361
375
 
376
+ > **NOTE:** There is no cancel endpoint. A session reaches `canceled` when the provider
377
+ > says so through its webhook, and `expired` when it passes `expires_at`.
378
+ > Neither is something a storefront drives.
379
+
362
380
  #### API
363
381
 
364
382
  **Create a Payment Session:**
@@ -441,7 +459,7 @@ console.log(completed.status) // 'completed'
441
459
  ```
442
460
 
443
461
  ```bash cURL
444
- curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payment_sessions/ps_xxx/complete' \
462
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx/payment_sessions/ps_xxx/complete' \
445
463
  -H 'X-Spree-API-Key: pk_xxx' \
446
464
  -H 'X-Spree-Token: CART_TOKEN' \
447
465
  -H 'Content-Type: application/json' \
@@ -487,10 +505,10 @@ class MyGateway < Spree::Gateway
487
505
 
488
506
  case event.type
489
507
  when 'payment.captured'
490
- session = Spree::PaymentSession.find_by(external_id: event.payment_id)
508
+ session = payment_sessions.find_by(external_id: event.payment_id)
491
509
  { action: :captured, payment_session: session }
492
510
  when 'payment.failed'
493
- session = Spree::PaymentSession.find_by(external_id: event.payment_id)
511
+ session = payment_sessions.find_by(external_id: event.payment_id)
494
512
  { action: :failed, payment_session: session }
495
513
  else
496
514
  nil # unsupported event
@@ -503,48 +521,55 @@ Supported actions: `:captured`, `:authorized`, `:failed`, `:canceled`.
503
521
 
504
522
  ### Payment
505
523
 
506
- Once a Payment Session is completed, Spree creates a `Payment` record (`Spree::Payment`) to track the result. The Payment is linked to the session via `response_code` matching the session's `external_id`.
524
+ Once a payment session completes, Spree creates a payment to record the result,
525
+ linked to the session by `response_code` matching its `external_id`.
507
526
 
508
527
  #### Attributes
509
528
 
510
- | Attribute | Description | Example Value |
511
- |-------------------|-----------------------------------------------------------------------------|---------------------|
512
- | `number` | A unique identifier for the payment. | `PAB12CD34` |
513
- | `source_type` | The type of source used for the payment. | `Spree::CreditCard` |
514
- | `source_id` | The ID of the source used for the payment. | `1` |
515
- | `amount` | The amount of the payment. | `99.99` |
516
- | `payment_method_id`| The ID of the payment method used. | `2` |
517
- | `state` | The current state of the payment (e.g., processing, completed, failed). | `completed` |
518
- | `response_code` | The gateway transaction ID. Links to the Payment Session's `external_id`. | `pi_3ABC123` |
519
- | `avs_response` | The address verification system response code. | `D` |
520
-
521
- #### Payment States
522
-
523
- After a Payment Session completes, the resulting Payment transitions through these states:
524
-
525
- ```mermaid
526
- stateDiagram-v2
527
- [*] --> checkout
528
- checkout --> processing : process!
529
- processing --> completed : purchase! succeeds
530
- processing --> pending : authorize! succeeds
531
- processing --> failed : payment declined
532
- pending --> completed : capture!
533
- pending --> void : void!
534
- completed --> void : void!
535
- checkout --> void : void!
536
- ```
537
-
538
- | State | Description |
539
- |-------|-------------|
540
- | `checkout` | Initial state. The payment has been created but not yet processed. |
541
- | `processing` | The payment is being processed (temporary - prevents double submission). |
542
- | `pending` | The payment has been authorized but not yet captured. Awaiting manual or scheduled capture. |
543
- | `failed` | The payment was rejected (e.g., card declined, insufficient funds). |
544
- | `void` | The payment has been voided and should not be counted against the order. |
545
- | `completed` | The payment has been captured. Only payments in this state count against the order total. |
546
-
547
- With **auto-capture** enabled (default for most gateways), the Payment goes directly from `checkout` → `processing` → `completed`. With **manual capture**, it stops at `pending` until an admin captures it.
529
+ These are the fields the Store API returns:
530
+
531
+ | Attribute | Description | Example |
532
+ |---|---|---|
533
+ | `number` | Derived from the owner — see above. | `R1001-P1` |
534
+ | `status` | Where the payment stands. | `completed` |
535
+ | `amount` / `display_amount` | The amount, raw and formatted for its currency. | `99.99` / `$99.99` |
536
+ | `payment_method_id` | Which method took it. | `pm_xyz789` |
537
+ | `source_type` | `credit_card`, `store_credit` or `payment_source`. | `credit_card` |
538
+ | `source_id` | The source's ID. | `cc_abc123` |
539
+ | `source` | The source itself, embedded. | `{ brand: "visa", last4: "4242" }` |
540
+ | `response_code` | The gateway's transaction reference. | `pi_3ABC123` |
541
+
542
+ > **NOTE:** `source_type` is a plain name — `credit_card`, not a class name — so you can
543
+ > switch on it directly.
544
+
545
+ #### Payment statuses
546
+
547
+ | Status | Meaning |
548
+ |---|---|
549
+ | `checkout` | Created, not yet processed. |
550
+ | `processing` | Being processed. Brief, and it stops a double submission. |
551
+ | `pending` | Authorized but not captured. Waiting for capture. |
552
+ | `completed` | Captured. Only these count towards the order total. |
553
+ | `failed` | The gateway rejected it. |
554
+ | `void` | Voided; it no longer counts against the order. |
555
+ | `invalid` | Superseded — written to an old checkout payment when a new one replaces it. |
556
+
557
+ When the money is taken follows the store's `capture_method`, which a payment
558
+ method can override for its own payments:
559
+
560
+ | `capture_method` | What happens |
561
+ |---|---|
562
+ | `checkout` | Charged when the order is placed — the payment goes straight to `completed`. |
563
+ | `on_dispatch` | Authorized at checkout, captured when the goods go out. |
564
+ | `manual` | Authorized at checkout; staff capture it when they choose. |
565
+
566
+ The last two leave the payment at `pending` until capture. See
567
+ [Configuration](../customization/configuration.md) for where the store
568
+ setting lives.
569
+
570
+ > **NOTE:** There is no state machine behind these. A payment's status is written by the
571
+ > workflows that process, capture and void it, so nothing transitions on its own
572
+ > and there is no sequence a client has to drive.
548
573
 
549
574
  #### Order Payment Status
550
575
 
@@ -563,11 +588,27 @@ Each payment update also recalculates the order's `payment_status`, derived from
563
588
 
564
589
  > **WARNING:** Keep an eye on orders sitting at `authorized` or `partially_paid` long after placement — a sudden increase can indicate a problem with your payment gateway that is affecting customers. Check the gateway's own dashboard for recent transactions.
565
590
 
591
+ ## Refunds
592
+
593
+ Refunds are an Admin API operation — there is no Store API route, so a customer
594
+ cannot start one from your storefront:
595
+
596
+ ```typescript Admin SDK
597
+ await adminClient.orders.refunds.create(orderId, {
598
+ payment_id: 'py_abc123',
599
+ amount: '25.00',
600
+ reason_id: 'rr_xyz789',
601
+ })
602
+ ```
603
+
604
+ A refund publishes `payment.refunded` and moves the order's `payment_status` to
605
+ `partially_refunded` or `refunded`.
606
+
566
607
  ## Payment Sources
567
608
 
568
609
  Payment sources represent the actual instrument used for a payment. They are created automatically when a Payment Session completes.
569
610
 
570
- ### Credit Cards (`Spree::CreditCard`)
611
+ ### Saved cards
571
612
 
572
613
  Stores non-sensitive credit card information. With modern gateways, the actual card data is tokenized by the provider - Spree only stores reference IDs and display information.
573
614
 
@@ -575,9 +616,10 @@ Stores non-sensitive credit card information. With modern gateways, the actual c
575
616
  |---------------------|-----------------------------------------------------------------------------------------------|------------------------|
576
617
  | `month` | The month the credit card expires. | `6` |
577
618
  | `year` | The year the credit card expires. | `2026` |
578
- | `cc_type` | The type of credit card (e.g., visa, mastercard). | `visa` |
579
- | `last_digits` | The last four digits of the credit card number. | `1234` |
580
- | `name` | The name of the credit card holder. | `John Doe` |
619
+ | `brand` | The card brand. | `visa` |
620
+ | `last4` | The last four digits. | `4242` |
621
+ | `name` | The cardholder's name. | `John Doe` |
622
+ | `default` | Whether this is the customer's default card. | `true` |
581
623
  | `gateway_payment_profile_id` | The payment token from the gateway (e.g., Stripe `pm_xxx`, Adyen `storedPaymentMethodId`). | `pm_1ABC123` |
582
624
 
583
625
  > **NOTE:** Spree never stores full credit card numbers. With modern gateways, card data is collected entirely by the gateway's frontend SDK (e.g., Stripe.js, Adyen Drop-in) and never touches your server. Spree only stores the tokenized reference (`gateway_payment_profile_id`) returned by the provider.
@@ -586,7 +628,7 @@ Stores non-sensitive credit card information. With modern gateways, the actual c
586
628
 
587
629
  A generic payment source model for non-card payment methods such as digital wallets, bank transfers, and buy-now-pay-later services. Gateway integrations create subtypes for each payment method type (e.g., Klarna, Afterpay, iDEAL, Apple Pay, Google Pay, PayPal).
588
630
 
589
- ### Gateway Customers (`Spree::GatewayCustomer`)
631
+ ### Gateway customers
590
632
 
591
633
  Maps a Spree customer to their provider-specific customer profile. This enables features like saved payment methods, recurring billing, and customer-level fraud detection.
592
634
 
@@ -594,13 +636,16 @@ Maps a Spree customer to their provider-specific customer profile. This enables
594
636
  |-----------|-------------|---------------|
595
637
  | `profile_id` | The provider's customer ID (encrypted at rest) | `cus_ABC123` |
596
638
  | `payment_method_id` | The gateway this customer belongs to | `1` |
597
- | `user_id` | The Spree user | `42` |
639
+ | `customer_id` | The Spree customer | `42` |
640
+
641
+ A customer has at most one record per payment method, and `profile_id` is encrypted with Active Record Encryption where it is configured.
598
642
 
599
- Each customer has at most one `GatewayCustomer` record per payment method. The `profile_id` is encrypted using Active Record Encryption when available.
643
+ > **NOTE:** This one is internal — the API does not expose it. It is listed here because
644
+ > gateway integrations rely on it; nothing a storefront does will read it.
600
645
 
601
646
  ## Payment Setup Sessions
602
647
 
603
- Payment Setup Sessions (`Spree::PaymentSetupSession`) allow customers to save payment methods for future use **without making an immediate payment**. This maps to concepts like Stripe's SetupIntent - a secure way to collect and tokenize payment details for later charges.
648
+ Payment setup sessions let customers save payment methods for future use **without making an immediate payment**. This maps to concepts like Stripe's SetupIntent - a secure way to collect and tokenize payment details for later charges.
604
649
 
605
650
  ### Use Cases
606
651
 
@@ -714,7 +759,7 @@ console.log(completed.payment_source_id) // e.g. 'card_xyz789' - the saved paym
714
759
  ```
715
760
 
716
761
  ```bash cURL
717
- curl -X POST 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_sessions/pss_abc123/complete' \
762
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_sessions/pss_abc123/complete' \
718
763
  -H 'X-Spree-API-Key: pk_xxx' \
719
764
  -H 'Authorization: Bearer $CUSTOMER_JWT' \
720
765
  -H 'Content-Type: application/json' \
@@ -722,7 +767,7 @@ curl -X POST 'https://api.mystore.com/api/v3/store/customers/me/payment_setup_se
722
767
  ```
723
768
 
724
769
 
725
- 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.
770
+ Spree verifies the result with the provider and saves a payment source — usually a card — for future payments.
726
771
 
727
772
  ## Supported Gateways
728
773
 
@@ -741,7 +786,11 @@ Spree publishes events throughout the payment lifecycle that you can subscribe t
741
786
  ### Payment Events
742
787
  | Event | Description |
743
788
  |-------|-------------|
744
- | `payment.paid` | Payment was completed |
789
+ | `payment.completed` | Payment reached `completed` |
790
+ | `payment.paid` | Payment was paid |
791
+ | `payment.captured` | An authorized payment was captured |
792
+ | `payment.voided` | Payment was voided |
793
+ | `payment.refunded` | Payment was refunded |
745
794
  | `order.paid` | Order is fully paid |
746
795
 
747
796
  ### Payment Session Events
@@ -756,9 +805,11 @@ Spree publishes events throughout the payment lifecycle that you can subscribe t
756
805
  ### Payment Setup Session Events
757
806
  | Event | Description |
758
807
  |-------|-------------|
808
+ | `payment_setup_session.processing` | Setup is being processed |
759
809
  | `payment_setup_session.completed` | Setup completed, payment source saved |
760
810
  | `payment_setup_session.failed` | Setup failed |
761
811
  | `payment_setup_session.canceled` | Setup was canceled |
812
+ | `payment_setup_session.expired` | Setup expired |
762
813
 
763
814
  See [Events](events.md) for more details on subscribing to events.
764
815
 
@@ -308,7 +308,21 @@ The scheduler is the host app's. Two jobs fan out per seller and the interval lo
308
308
  | `Spree::SellerPayouts::SweepDueJob` | daily | settles every seller who is due |
309
309
  | `Spree::SellerTransfers::ExecutePendingDueJob` | hourly | retries earnings a provider refused — usually a passing thing |
310
310
 
311
- `spree/spree-starter` ships both entries in `server/config/recurring.yml`. A seller on the `manual` interval is skipped by the sweep and settled by hand instead:
311
+ > **WARNING:** **You have to schedule these yourself** — a new project does not ship them, so
312
+ > a marketplace that skips this step never pays a seller. Add both to your
313
+ > scheduler; with Solid Queue that is `server/config/recurring.yml`:
314
+ >
315
+ > ```yaml server/config/recurring.yml
316
+ production:
317
+ sweep_due_seller_payouts:
318
+ class: Spree::SellerPayouts::SweepDueJob
319
+ schedule: every day at 3am
320
+ execute_pending_seller_transfers:
321
+ class: Spree::SellerTransfers::ExecutePendingDueJob
322
+ schedule: every hour
323
+ ```
324
+
325
+ A seller on the `manual` interval is skipped by the sweep and settled by hand instead:
312
326
 
313
327
 
314
328
  ```typescript Admin SDK
@@ -319,7 +333,7 @@ const { data: balances } = await adminClient.sellers.balances('sel_xxx')
319
333
  await adminClient.sellers.settle('sel_xxx')
320
334
 
321
335
  // The built-in provider: record that the bank transfer went out
322
- await adminClient.sellerPayouts.complete('spo_xxx', { reference: 'TRN-2026-0912-0042' })
336
+ await adminClient.sellerPayouts.complete('vpo_xxx', { reference: 'TRN-2026-0912-0042' })
323
337
  ```
324
338
 
325
339
  ```bash cURL
@@ -329,14 +343,43 @@ curl 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/balances' \
329
343
  curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/payouts' \
330
344
  -H 'X-Spree-API-Key: sk_xxx'
331
345
 
332
- curl -X PATCH 'https://api.mystore.com/api/v3/admin/seller_payouts/spo_xxx/complete' \
346
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/seller_payouts/vpo_xxx/complete' \
333
347
  -H 'X-Spree-API-Key: sk_xxx' \
334
348
  -H 'Content-Type: application/json' \
335
349
  -d '{ "reference": "TRN-2026-0912-0042" }'
336
350
  ```
337
351
 
338
352
 
339
- `GET /admin/seller_transfers` and `GET /admin/seller_payouts` list the ledger across every seller; the seller panel reads its own through `GET /seller/balances`, `/seller/transfers` and `/seller/payouts`, all read-only — the ledger is written by fulfilment and the sweep, never by the seller.
353
+ Settling answers `201` with the payouts it created, one per currency. When one
354
+ currency settles and another is refused it still answers `201`, listing what
355
+ failed under `meta.failures` — so check that rather than assuming success. With
356
+ nothing to settle it answers `422`.
357
+
358
+ ### Reading the ledger
359
+
360
+ Across every seller, from the marketplace side:
361
+
362
+ ```typescript Admin SDK
363
+ const { data: transfers } = await adminClient.sellerTransfers.list({ seller_id_eq: 'sel_xxx' })
364
+ const { data: payouts } = await adminClient.sellerPayouts.list({ status_eq: 'completed' })
365
+
366
+ const payout = await adminClient.sellerPayouts.get('vpo_xxx')
367
+ ```
368
+
369
+ And from the seller's own panel, where every request is already scoped to the
370
+ seller signed in — which is why none of these takes a seller ID:
371
+
372
+ ```typescript Seller SDK
373
+ // What I'm owed, one row per currency
374
+ const { data: balances } = await sellerClient.balances.list()
375
+
376
+ // What I earned, and which payout settled it
377
+ const { data: transfers } = await sellerClient.transfers.list({ payout_id_eq: 'vpo_xxx' })
378
+ const { data: payouts } = await sellerClient.payouts.list()
379
+ ```
380
+
381
+ All of it is read-only: the ledger is written by fulfilment and the sweep, never
382
+ by a seller.
340
383
 
341
384
  ### Payout providers
342
385
 
@@ -347,7 +390,9 @@ A provider is a stateless class registered in `Spree.payout_providers` and chose
347
390
  | `Spree::PayoutProvider::System` (default) | No — keeps the books | The operator marks each payout paid, with the bank reference |
348
391
  | Stripe Connect (`spree_stripe`) | Yes — transfers each earning to the seller's connected account as goods ship, tied to the charge that funds it | A Stripe webhook, when the money lands |
349
392
 
350
- The contract is small: `transfer!` credits one earning, `pay!` sends one settlement, `reverse!` takes back part of an earning after a refund, and every call carries an idempotency key derived from the ledger row so a retry finds the movement it already made. Providers that need the seller to hold an account with them answer `requires_payout_account?` and implement `onboarding_url` — which is what the **payout account** [onboarding requirement](#onboarding-requirements) drives. To pay sellers through rails of your own, see [Seller payouts](../providers/payouts.md).
393
+ The contract is small: `transfer!` credits one earning, `pay!` sends one settlement, `reverse!` takes back part of an earning after a refund, and every call carries an idempotency key derived from the ledger row so a retry finds the movement it already made. Providers that need the seller to hold an account with them answer `requires_payout_account?` and implement `onboarding_url` — which is what the **payout account** [onboarding requirement](#onboarding-requirements) drives. A seller gets a fresh link from `POST /seller/onboarding/payout_account`, and `GET /admin/payout_providers` lists what is registered, for a picker.
394
+
395
+ A connected provider confirms settlements through `POST /api/v3/webhooks/payouts/:payment_method_id` — separate from the payment webhook, because providers scope seller-account events to their own subscription and signing secret. To pay sellers through rails of your own, see [Seller payouts](../providers/payouts.md).
351
396
 
352
397
  See [Stripe Connect for marketplaces](../../integrations/payments/stripe-connect.md) for the shipped provider.
353
398
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.241",
3
+ "version": "0.1.242",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",