@spree/docs 0.1.240 → 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.
- package/dist/api-reference/store-api/metadata.md +6 -6
- package/dist/developer/core-concepts/architecture.md +15 -19
- package/dist/developer/core-concepts/carts.md +71 -1
- package/dist/developer/core-concepts/delivery-setup.md +3 -1
- package/dist/developer/core-concepts/freight.md +279 -0
- package/dist/developer/core-concepts/metafields.md +0 -2
- package/dist/developer/core-concepts/payments.md +123 -72
- package/dist/developer/core-concepts/sellers.md +50 -5
- package/dist/developer/create-spree-app/quickstart.md +22 -19
- package/dist/developer/customization/api.md +2 -2
- package/dist/developer/customization/checkout.md +117 -0
- package/dist/developer/customization/configuration.md +76 -175
- package/dist/developer/customization/decorators.md +120 -130
- package/dist/developer/customization/metadata.md +9 -9
- package/dist/developer/customization/model-preferences.md +34 -68
- package/dist/developer/customization/permissions.md +13 -28
- package/dist/developer/customization/validations.md +16 -43
- package/dist/developer/getting-started/quickstart.md +25 -24
- package/dist/developer/how-to/custom-promotion.md +1 -1
- package/dist/developer/how-to/custom-search-provider.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +2 -2
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/configuration.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/testing.md +45 -91
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
62
|
-
string
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
|
513
|
-
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
517
|
-
| `
|
|
518
|
-
| `
|
|
519
|
-
| `
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
| `
|
|
542
|
-
|
|
543
|
-
| `
|
|
544
|
-
| `
|
|
545
|
-
| `
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
###
|
|
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
|
-
| `
|
|
579
|
-
| `
|
|
580
|
-
| `name` | The name
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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('
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -18,7 +18,7 @@ The CLI walks you through an interactive setup:
|
|
|
18
18
|
|
|
19
19
|
Once complete, your store is running at [http://localhost:3000](http://localhost:3000) — setup pulls the latest Spree image, seeds the database, and configures API keys, then prints a summary with your admin credentials and keys. If you skipped starting services, the first `pnpm dev` completes setup automatically.
|
|
20
20
|
|
|
21
|
-
The **
|
|
21
|
+
The **dashboard** and the marketplace **seller panel** are included in every project — there is no flag to add them.
|
|
22
22
|
|
|
23
23
|
## Prerequisites
|
|
24
24
|
|
|
@@ -35,7 +35,7 @@ npx create-spree-app@latest my-store --no-storefront --no-sample-data --no-start
|
|
|
35
35
|
|
|
36
36
|
| Flag | Description |
|
|
37
37
|
|------|-------------|
|
|
38
|
-
| `--react-dashboard` |
|
|
38
|
+
| `--react-dashboard` | No-op — the dashboard is always included. Kept so existing scripts keep working |
|
|
39
39
|
| `--no-storefront` | Skip Next.js storefront setup |
|
|
40
40
|
| `--no-sample-data` | Skip loading sample products and categories |
|
|
41
41
|
| `--no-start` | Don't start Docker services after scaffolding |
|
|
@@ -52,29 +52,32 @@ npx create-spree-app@latest my-store --no-storefront --no-sample-data --no-start
|
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
54
|
my-store/
|
|
55
|
+
├── server/ # The Spree API — a full Rails app from spree/spree-starter
|
|
56
|
+
│ ├── app/ # Your models, controllers, serializers and jobs
|
|
57
|
+
│ ├── config/
|
|
58
|
+
│ ├── Dockerfile # Also builds the production image (API + dashboard)
|
|
59
|
+
│ └── Gemfile
|
|
60
|
+
├── apps/
|
|
61
|
+
│ ├── dashboard/ # Admin dashboard (React)
|
|
62
|
+
│ │ └── .env.local # Dev proxy target — no credentials
|
|
63
|
+
│ ├── seller-dashboard/ # Seller panel, for marketplaces (React)
|
|
64
|
+
│ └── storefront/ # Next.js storefront (unless --no-storefront)
|
|
65
|
+
│ └── .env.local # API URL + publishable key
|
|
66
|
+
├── .env # SECRET_KEY_BASE, SPREE_PORT, SPREE_VERSION_TAG, SPREE_SAMPLE_DATA
|
|
67
|
+
├── .spree/ # CLI credentials — gitignored, created on first run
|
|
55
68
|
├── docker-compose.yml # Spree backend (prebuilt image) + Postgres + Meilisearch
|
|
56
69
|
├── docker-compose.dev.yml # Alternative: build from local server/
|
|
57
70
|
├── render.yaml # Render Blueprint — one Docker service built from this repo
|
|
58
|
-
├── .env # SECRET_KEY_BASE, SPREE_PORT, SPREE_VERSION_TAG, SPREE_SAMPLE_DATA
|
|
59
71
|
├── .dockerignore # Keeps the production build context to sources
|
|
60
72
|
├── .gitignore
|
|
61
|
-
├── package.json #
|
|
62
|
-
├──
|
|
63
|
-
├──
|
|
64
|
-
|
|
65
|
-
│ ├── Dockerfile # Also builds the production image (API + dashboard)
|
|
66
|
-
│ ├── config/
|
|
67
|
-
│ ├── app/
|
|
68
|
-
│ └── ...
|
|
69
|
-
└── apps/
|
|
70
|
-
├── storefront/ # Next.js storefront (unless --no-storefront)
|
|
71
|
-
│ ├── .env.local # API URL + API key
|
|
72
|
-
│ └── ...
|
|
73
|
-
└── dashboard/ # React Dashboard (with --react-dashboard)
|
|
74
|
-
├── .env.local # Dev proxy target — no credentials
|
|
75
|
-
└── ...
|
|
73
|
+
├── package.json # Scripts and the pinned @spree/cli
|
|
74
|
+
├── AGENTS.md # Instructions for coding agents
|
|
75
|
+
├── CLAUDE.md # Project layout and conventions
|
|
76
|
+
└── README.md # How to run everything
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
**`server/` is the only part that is Spree's.** The apps under `apps/` are ordinary Vite and Next.js projects you own — restyle, restructure or replace them without forking anything. Each carries its own instructions (`server/CLAUDE.md`, `apps/dashboard/AGENTS.md`), which is also what a coding agent reads.
|
|
80
|
+
|
|
78
81
|
### What's in docker-compose.yml
|
|
79
82
|
|
|
80
83
|
- **Spree** — one `web` container running the `ghcr.io/spree/spree:latest` image on the configured port (default `3000`); background jobs run in-process via Solid Queue (stored in Postgres — job dashboard at `/jobs`)
|
|
@@ -127,7 +130,7 @@ Open [http://localhost:3000/admin](http://localhost:3000/admin) and log in with:
|
|
|
127
130
|
| **Email** | `spree@example.com` |
|
|
128
131
|
| **Password** | `spree123` |
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), with the same credentials, live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
|
|
131
134
|
|
|
132
135
|
### Store API
|
|
133
136
|
|
|
@@ -5,7 +5,7 @@ description: Add new Store API endpoints, customize existing JSON responses with
|
|
|
5
5
|
|
|
6
6
|
Before you start customizing Spree API endpoints, make sure you reviewed all existing API endpoints in the [Spree API docs](../../api-reference/admin-api/endpoints.md).
|
|
7
7
|
|
|
8
|
-
For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/
|
|
8
|
+
For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/model-and-api.md).
|
|
9
9
|
|
|
10
10
|
## Customizing JSON Responses
|
|
11
11
|
|
|
@@ -35,7 +35,7 @@ Restart the server and the Product API will include your new attribute.
|
|
|
35
35
|
|
|
36
36
|
### Adding an Association
|
|
37
37
|
|
|
38
|
-
Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/
|
|
38
|
+
Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/model-and-api.md) for the full example).
|
|
39
39
|
|
|
40
40
|
Create a serializer for the new model:
|
|
41
41
|
|