Back to Spree

Payments

docs/developer/core-concepts/payments.mdx

5.4.232.5 KB
Original Source

Overview

Spree has a highly flexible payments model which allows multiple payment methods to be available during the checkout. The logic for processing payments is decoupled from orders, making it easy to define custom payment methods with their own processing logic.

Payment methods typically represent a payment gateway. Gateways will process card payments, online bank transfers, buy-now-pay-later, wallet payments, and other types of payments. Spree also comes with a Check option for offline processing.

The Payment model in Spree tracks payments against Orders. Payments relate to a source which indicates how the payment was made, and a PaymentMethod, indicating the processor used for this payment.

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.

Payment Architecture Diagram

mermaid
erDiagram
    Payment {
        string number
        decimal amount
        string state
        string response_code
        string avs_response
        string source_type
        string source_id
    }

    PaymentMethod {
        string name
        string type
        string description
        boolean active
        string display_on
        integer position
    }

    PaymentSession {
        string status
        decimal amount
        string currency
        string external_id
        json external_data
        datetime expires_at
    }

    PaymentSetupSession {
        string status
        string external_id
        string external_client_secret
        json external_data
    }

    PaymentSource {
        string type
        string gateway_payment_profile_id
    }

    CreditCard {
        string last_digits
        string cc_type
        integer month
        integer year
        string name
    }

    GatewayCustomer {
        string profile_id
    }

    StoreCredit {
        decimal amount
        decimal amount_used
        decimal amount_authorized
        string currency
    }

    Refund {
        decimal amount
        string reason
    }

    LogEntry {
        text details
    }

    Order ||--o{ Payment : "has many"
    Order ||--o{ PaymentSession : "has many"
    Payment }o--|| PaymentMethod : "belongs to"
    Payment }o--o| CreditCard : "source"
    Payment }o--o| PaymentSource : "source"
    Payment }o--o| StoreCredit : "source"
    Payment ||--o| PaymentSession : "linked via response_code"
    Payment ||--o{ LogEntry : "has many"
    Payment ||--o{ Refund : "has many"
    PaymentMethod ||--o{ PaymentSession : "has many"
    PaymentMethod ||--o{ PaymentSetupSession : "has many"
    PaymentMethod ||--o{ GatewayCustomer : "has many"
    PaymentMethod }o--|| Store : "belongs to"
    PaymentSetupSession }o--o| PaymentSource : "creates"
    PaymentSetupSession }o--|| Customer : "belongs to"
    GatewayCustomer }o--|| Customer : "belongs to"
    CreditCard }o--|| Customer : "belongs to"
    StoreCredit }o--|| Customer : "belongs to"
    Refund }o--|| Payment : "belongs to"
    Refund }o--|| Reimbursement : "belongs to"

Key relationships:

  • Payment tracks each payment attempt against an Order
  • Payment Method defines how payments are processed (Stripe, Adyen, PayPal, Check, etc.)
  • Payment Session manages the gateway-side payment lifecycle (e.g., Stripe PaymentIntent, Adyen Session)
  • Payment Setup Session manages saving payment methods for future use without an immediate charge (e.g., Stripe SetupIntent)
  • Source is polymorphic - can be a Credit Card, Payment Source (for alternative methods like Klarna, iDEAL), or Store Credit
  • Gateway Customer stores the provider-specific customer profile (e.g., Stripe Customer ID)
  • Log Entries record gateway responses for debugging
  • Refunds track money returned to customers

Payment Methods

Payment methods represent the different options a customer has for making a payment. Most sites will accept credit card payments through a payment gateway, but there are other options. Spree also comes with built-in support for a Check payment, which can be used to represent any offline payment. Gateway providers such as Stripe, Adyen, and PayPal provide a wide range of payment methods, including credit cards, bank transfers, buy-now-pay-later, and digital wallets (Apple Pay, Google Pay, etc.).

A PaymentMethod can have the following attributes:

AttributeDescriptionExample
typeThe payment method typeCheck
nameThe visible name for this payment methodCheck
descriptionThe description for this payment methodPay by check.
activeWhether or not this payment method is active. Set it false to hide it in the Store API.true
display_onDetermines where the payment method can be visible. Values can be front for Store API, back for admin panel only or both for both.both
positionThe position of the payment method in lists. Lower numbers appear first.1
<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. </Info>

Session-based vs Legacy Payment Methods

Payment methods indicate whether they use the modern session-based flow via the session_required? method:

MethodDescriptionDefault
session_required?Returns true if this payment method requires a Payment Session for processing.false
setup_session_supported?Returns true if this payment method supports saving payment methods for future use (Payment Setup Sessions).false
payment_session_classReturns the STI subclass of Spree::PaymentSession for this gateway (e.g., Spree::PaymentSessions::Stripe).nil
payment_setup_session_classReturns the STI subclass of Spree::PaymentSetupSession for this gateway.nil

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.

Non-Session Payment Methods (Manual/Offline)

Payment methods where session_required? returns false don't need a payment session. These are typically offline or manual payment methods such as:

  • Check — built-in (Spree::PaymentMethod::Check)
  • Cash on Delivery — customer pays upon delivery
  • Bank Transfer / Wire — customer transfers money to a bank account
  • Purchase Order — common in B2B, customer provides a PO number

For these methods, the Store API allows creating a payment directly without going through the payment session flow:

typescript
const options = { spreeToken: cart.token }

// Create a payment for a non-session payment method
const payment = await client.carts.payments.create(cart.id, {
  payment_method_id: 'pm_xyz789',
  amount: '99.99',              // Optional, defaults to order total minus store credits
  metadata: {                   // Optional, write-only metadata (e.g. PO number)
    purchase_order_number: 'PO-12345',
  },
}, options)

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.

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).

Payment Flow

Spree supports two payment flows depending on the payment method type:

Session-Based Flow (Stripe, Adyen, PayPal, etc.)

Modern payment gateways use a three-phase approach: first a Payment Session is created with the gateway, then the customer completes payment on the frontend, and finally the order is completed via an explicit API call. Payment processing and order completion are intentionally separated — this prevents race conditions and ensures reliable checkout regardless of payment method type (cards, wallets, offsite redirects).

mermaid
sequenceDiagram
    participant Frontend
    participant Spree API
    participant Payment Provider

    rect rgb(240, 245, 255)
    note right of Frontend: Phase 1: Payment Session
    Frontend->>Spree API: Create Payment Session
    Spree API->>Payment Provider: Create session (PaymentIntent/Session)
    Payment Provider-->>Spree API: Session ID + client_secret
    Spree API-->>Frontend: PaymentSession (pending)
    end

    rect rgb(240, 255, 240)
    note right of Frontend: Phase 2: Customer Pays
    Frontend->>Payment Provider: Collect payment (using client_secret)
    Note over Frontend,Payment Provider: 3DS / offsite redirect handled here
    Payment Provider-->>Frontend: Payment result
    end

    rect rgb(255, 248, 240)
    note right of Frontend: Phase 3: Complete Payment Session
    Frontend->>Spree API: Complete Payment Session
    Spree API->>Payment Provider: Verify payment status
    Spree API->>Spree API: Create Payment record
    Spree API-->>Frontend: PaymentSession (completed)
    end

    rect rgb(255, 240, 245)
    note right of Frontend: Phase 4: Complete Order
    Frontend->>Spree API: POST /carts/:id/complete
    Spree API->>Spree API: Validate & finalize order
    Spree API-->>Frontend: Completed order
    end
<Steps> <Step title="Create Payment Session"> The frontend calls the API to create a Payment Session for a specific payment method and order. Spree calls the gateway to create a provider-side session (e.g., Stripe PaymentIntent, Adyen Session) and returns the session data including a `client_secret` for the frontend SDK.
<Info>
The payment session should be created (or recreated) **after** the shipping method is selected, so the amount includes shipping costs. If the order total changes (e.g., customer selects a different shipping rate or applies a coupon), create a new payment session with the updated amount.
</Info>
</Step> <Step title="Customer pays on the frontend"> 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. </Step> <Step title="Complete Payment Session"> After the customer completes payment, the frontend calls the Complete Payment Session endpoint. Spree verifies the payment status with the gateway, creates a `Payment` record, creates the appropriate payment source (Credit Card, wallet, etc.), and marks the session as completed.
**This step does NOT complete the order** — it only handles payment processing. For wallet payments (Apple Pay, Google Pay), the gateway also patches the order's billing address with data from the wallet at this stage.
</Step> <Step title="Complete Order"> The frontend calls `POST /carts/:id/complete` to finalize the order. Spree validates the order is ready (addresses, shipments, payment), advances through any remaining checkout states, and marks the order as complete.
This separation ensures the same flow works for all payment types — inline cards, offsite redirects, and wallet payments.
</Step> </Steps>

Offsite Payment Flow (CashApp, 3D Secure, Klarna, etc.)

For payment methods that redirect the customer away from your site, use an intermediate confirm-payment page:

mermaid
sequenceDiagram
    participant Frontend
    participant Gateway
    participant Spree API

    Frontend->>Gateway: confirmPayment (redirects to gateway)
    Gateway-->>Frontend: Redirect back to /confirm-payment/:id?session=...
    Frontend->>Spree API: Complete Payment Session
    Spree API-->>Frontend: Session completed
    Frontend->>Spree API: POST /carts/:id/complete
    Spree API-->>Frontend: Order completed
    Frontend->>Frontend: Redirect to thank-you page

Webhook-Driven Completion (Browser Closed)

If the customer closes the browser after paying but before the frontend calls complete, Spree handles this via payment webhooks:

mermaid
sequenceDiagram
    participant Payment Provider
    participant Spree API

    Payment Provider->>Spree API: POST /api/v3/webhooks/payments/:pm_id
    Spree API->>Spree API: Verify signature
    Spree API->>Spree API: Enqueue HandleWebhookJob
    Spree API-->>Payment Provider: 200 OK

    note over Spree API: Async processing
    Spree API->>Spree API: Create/update Payment
    Spree API->>Spree API: Complete order via Carts::Complete

Gateway extensions implement parse_webhook_event to normalize provider-specific payloads into a standard format. Spree core handles the rest — creating the payment record, completing the session, and finalizing the order.

Direct Payment Flow (Check, Cash on Delivery, Bank Transfer, etc.)

Non-session payment methods use a simpler flow where a payment is created directly without involving an external payment provider:

mermaid
sequenceDiagram
    participant Frontend
    participant Spree API

    Frontend->>Spree API: GET /payment_methods
    Spree API-->>Frontend: Payment methods (with session_required flag)

    Frontend->>Spree API: POST /payments (payment_method_id)
    Spree API->>Spree API: Create Payment (checkout state)
    Spree API-->>Frontend: Payment created

    Frontend->>Spree API: PATCH /orders/:id/complete
    Spree API->>Spree API: process_payments! (authorize succeeds immediately)
    Spree API->>Spree API: Payment → pending/completed
    Spree API-->>Frontend: Order completed
<Steps> <Step title="List payment methods"> The frontend calls `GET /payment_methods` and checks the `session_required` flag on each method. Methods with `session_required: false` use this direct flow. </Step> <Step title="Create payment"> The frontend calls `POST /payments` with the `payment_method_id`. Spree creates a `Payment` record in `checkout` state. No external provider interaction is needed. </Step> <Step title="Complete order"> 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. </Step> </Steps>

Payment Session

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.

Attributes

AttributeDescriptionExample Value
statusCurrent session state: pending, processing, completed, failed, canceled, expiredcompleted
amountThe payment amount99.99
currencyISO currency codeUSD
external_idThe provider-side session ID (e.g., Stripe PaymentIntent ID)pi_3ABC123
external_dataProvider-specific data including client_secret for frontend SDK{"client_secret": "pi_3ABC_secret_xyz"}
customer_external_idThe provider's customer IDcus_ABC123
expires_atWhen the session expires2025-01-01T12:00:00Z

States

mermaid
stateDiagram-v2
    [*] --> pending
    pending --> processing
    pending --> completed
    pending --> failed
    pending --> canceled
    pending --> expired
    processing --> completed
    processing --> failed
    processing --> canceled
    processing --> expired

API

Create a Payment Session:

typescript
const options = { spreeToken: cart.token }

// Create a payment session for the selected payment method
const session = await client.carts.paymentSessions.create(cart.id, {
  payment_method_id: 'pm_xyz789',
  amount: '99.99',             // Optional, defaults to order total
  external_data: {},           // Optional, provider-specific data
}, options)

// The session contains provider-specific data for the frontend SDK
console.log(session.external_id)              // e.g. 'pi_3ABC123' (Stripe PaymentIntent ID)
console.log(session.external_data.client_secret) // Use with Stripe.js or Adyen Drop-in

Response shape (StorePaymentSession):

json
{
  "id": "ps_abc123",
  "status": "pending",
  "amount": "99.99",
  "currency": "USD",
  "external_id": "pi_3ABC123",
  "external_data": {
    "client_secret": "pi_3ABC123_secret_xyz"
  },
  "customer_external_id": "cus_ABC123",
  "expires_at": "2025-01-01T12:00:00Z",
  "payment_method_id": "pm_xyz789",
  "order_id": "or_ABC123",
  "payment": null
}

Update a Payment Session (e.g., after order total changes):

typescript
const updated = await client.carts.paymentSessions.update(
  cart.id, session.id,
  { amount: '149.99' },
  options
)

Complete a Payment Session (after customer confirms payment on the frontend):

typescript
const completed = await client.carts.paymentSessions.complete(
  cart.id, session.id,
  { session_result: '...', external_data: {} },
  options
)
console.log(completed.status) // 'completed'
<Warning> Completing a payment session does **not** complete the order. You must call `POST /carts/:id/complete` separately after the session is completed. This separation prevents race conditions between the frontend and payment webhooks. </Warning>

Complete the Order (after the payment session is completed):

typescript
const order = await client.carts.complete(cart.id, options)

Payment Webhooks

Spree provides a generic webhook endpoint at POST /api/v3/webhooks/payments/:payment_method_id that payment gateway extensions can use. When a payment provider sends a webhook (e.g., Stripe payment_intent.succeeded), Spree:

  1. Verifies the webhook signature synchronously (returns 401 if invalid)
  2. Enqueues a background job to process the event
  3. Returns 200 OK immediately

The background job creates/updates the Payment record, marks the session as completed, and completes the order if needed.

Gateway Interface

Gateway extensions implement parse_webhook_event to normalize provider-specific payloads:

ruby
class MyGateway < Spree::Gateway
  def parse_webhook_event(raw_body, headers)
    # Verify signature — raise WebhookSignatureError if invalid
    event = verify_signature(raw_body, headers)

    case event.type
    when 'payment.captured'
      session = Spree::PaymentSession.find_by(external_id: event.payment_id)
      { action: :captured, payment_session: session }
    when 'payment.failed'
      session = Spree::PaymentSession.find_by(external_id: event.payment_id)
      { action: :failed, payment_session: session }
    else
      nil # unsupported event
    end
  end
end

Supported actions: :captured, :authorized, :failed, :canceled.

Payment

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.

Attributes

AttributeDescriptionExample Value
numberA unique identifier for the payment.P123456789
source_typeThe type of source used for the payment.Spree::CreditCard
source_idThe ID of the source used for the payment.1
amountThe amount of the payment.99.99
payment_method_idThe ID of the payment method used.2
stateThe current state of the payment (e.g., processing, completed, failed).completed
response_codeThe gateway transaction ID. Links to the Payment Session's external_id.pi_3ABC123
avs_responseThe address verification system response code.D

Payment States

After a Payment Session completes, the resulting Payment transitions through these states:

mermaid
stateDiagram-v2
    [*] --> checkout
    checkout --> processing : process!
    processing --> completed : purchase! succeeds
    processing --> pending : authorize! succeeds
    processing --> failed : payment declined
    pending --> completed : capture!
    pending --> void : void!
    completed --> void : void!
    checkout --> void : void!
StateDescription
checkoutInitial state. The payment has been created but not yet processed.
processingThe payment is being processed (temporary - prevents double submission).
pendingThe payment has been authorized but not yet captured. Awaiting manual or scheduled capture.
failedThe payment was rejected (e.g., card declined, insufficient funds).
voidThe payment has been voided and should not be counted against the order.
completedThe payment has been captured. Only payments in this state count against the order total.

With auto-capture enabled (default for most gateways), the Payment goes directly from checkoutprocessingcompleted. With manual capture, it stops at pending until an admin captures it.

Order Payment States

Each payment update also recalculates the order's payment_state:

Payment StateDescription
balance_duePayment is required for this order
failedThe last payment for the order failed
credit_owedThis order has been paid for in excess of its total
paidThis order has been paid for in full
<Warning> You may want to keep tabs on the number of orders with a `payment_state` of `failed`. A sudden increase could indicate a problem with your payment gateway and most likely a serious problem affecting customer satisfaction. Check the latest `log_entries` for the most recent payments if this is happening. </Warning>

Log Entries

Responses from payment gateways are stored as log entries for debugging purposes. These can be viewed in the Admin Panel on the payment detail page.

Payment Sources

Payment sources represent the actual instrument used for a payment. They are created automatically when a Payment Session completes.

Credit Cards (Spree::CreditCard)

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.

AttributeDescriptionExample Value
monthThe month the credit card expires.6
yearThe year the credit card expires.2026
cc_typeThe type of credit card (e.g., visa, mastercard).visa
last_digitsThe last four digits of the credit card number.1234
nameThe name of the credit card holder.John Doe
gateway_payment_profile_idThe payment token from the gateway (e.g., Stripe pm_xxx, Adyen storedPaymentMethodId).pm_1ABC123
<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. </Note>

Payment Sources

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).

Gateway Customers (Spree::GatewayCustomer)

Maps a Spree customer to their provider-specific customer profile. This enables features like saved payment methods, recurring billing, and customer-level fraud detection.

AttributeDescriptionExample Value
profile_idThe provider's customer ID (encrypted at rest)cus_ABC123
payment_method_idThe gateway this customer belongs to1
user_idThe Spree user42

Each customer has at most one GatewayCustomer record per payment method. The profile_id is encrypted using Active Record Encryption when available.

Payment Setup Sessions

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.

Use Cases

  • Saving a credit card to the customer's account for faster future checkouts
  • Authorizing a payment method for subscription billing
  • Adding a payment method during account onboarding (before any purchase)

How Payment Setup Sessions Work

mermaid
sequenceDiagram
    participant Frontend
    participant Spree API
    participant Payment Provider

    Frontend->>Spree API: Create Payment Setup Session
    Spree API->>Payment Provider: Create setup session (SetupIntent)
    Payment Provider-->>Spree API: Session ID + client_secret
    Spree API-->>Frontend: PaymentSetupSession (pending)

    Frontend->>Payment Provider: Collect card details (using client_secret)
    Note over Frontend,Payment Provider: 3DS verification if needed

    Frontend->>Spree API: Complete Payment Setup Session
    Spree API->>Payment Provider: Verify setup result
    Spree API->>Spree API: Create PaymentSource (saved card)
    Spree API-->>Frontend: Completed PaymentSetupSession

Payment Setup Session Attributes

AttributeDescriptionExample Value
statusCurrent session state: pending, processing, completed, failed, canceled, expiredcompleted
external_idThe provider-side session ID (e.g., Stripe SetupIntent ID)seti_ABC123
external_client_secretClient secret for the frontend SDKseti_ABC123_secret_xyz
external_dataProvider-specific data{}
payment_source_idThe saved payment source created after completionps_xyz789
payment_source_typeThe type of saved payment sourceSpree::CreditCard

Payment Setup Session API

<Note> Payment Setup Sessions require customer authentication. The customer must be logged in. </Note>

Create a Payment Setup Session:

typescript
const options = { token: jwtToken }

// Create a setup session for saving a payment method
const setupSession = await client.customer.paymentSetupSessions.create({
  payment_method_id: 'pm_xyz789',
  external_data: {},
}, options)

// Use the client secret with the gateway's frontend SDK
console.log(setupSession.external_client_secret) // e.g. 'seti_ABC123_secret_xyz'

Response shape (StorePaymentSetupSession):

json
{
  "id": "pss_abc123",
  "status": "pending",
  "external_id": "seti_ABC123",
  "external_client_secret": "seti_ABC123_secret_xyz",
  "external_data": {},
  "payment_method_id": "pm_xyz789",
  "payment_source_id": null,
  "payment_source_type": null,
  "customer_id": "usr_def456"
}

Get a Payment Setup Session:

typescript
const session = await client.customer.paymentSetupSessions.get('pss_abc123', options)

Complete a Payment Setup Session (after the customer completes setup on the frontend using the gateway SDK and external_client_secret):

typescript
const completed = await client.customer.paymentSetupSessions.complete(
  'pss_abc123',
  { external_data: {} },
  options
)
console.log(completed.status)             // 'completed'
console.log(completed.payment_source_id)  // e.g. 'ps_xyz789' - the saved payment method

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.

Supported Gateways

Spree team maintains several payment gateway integrations. All of these gateways are fully PCI compliant, using native gateway SDKs, meaning no sensitive payment data is stored or processed through Spree.

<Card title="Stripe" href="/integrations/payments/stripe" icon="link"> Stripe integration, supports all Stripe payment methods, including credit cards, bank transfers, Apple Pay, Google Pay, Klarna, Afterpay, and more. Also supports quick checkout. </Card> <Card title="Adyen" href="/integrations/payments/adyen" icon="link"> Adyen integration, supports all Adyen payment methods, including credit cards, bank transfers, Apple Pay, Google Pay, Klarna, and more. </Card> <Card title="PayPal" href="/integrations/payments/paypal" icon="link"> Native PayPal integration, supports PayPal, PayPal Credit, and PayPal Pay Later. </Card>

Payment Events

Spree publishes events throughout the payment lifecycle that you can subscribe to:

Payment Events

EventDescription
payment.paidPayment was completed
order.paidOrder is fully paid

Payment Session Events

EventDescription
payment_session.processingSession is being processed
payment_session.completedSession completed successfully
payment_session.failedSession processing failed
payment_session.canceledSession was canceled
payment_session.expiredSession expired

Payment Setup Session Events

EventDescription
payment_setup_session.completedSetup completed, payment source saved
payment_setup_session.failedSetup failed
payment_setup_session.canceledSetup was canceled

See Events for more details on subscribing to events.

Key Services

ServiceDescription
Spree::Carts::CompleteCompletes the order — validates, processes payments (if not already done), advances state machine. Used by both the POST /carts/:id/complete endpoint and the webhook handler.
Spree::Payments::HandleWebhookProcesses a normalized webhook event — creates Payment, marks session completed, calls Carts::Complete.
Spree::Payments::HandleWebhookJobBackground job that wraps HandleWebhook — enqueued by the webhook controller for async processing.

Both Carts::Complete and HandleWebhook are registered in Spree::Dependencies and can be replaced with custom implementations:

ruby
# config/initializers/spree.rb
Spree::Dependencies.carts_complete_service = 'MyApp::CustomCartComplete'
Spree::Dependencies.payments_handle_webhook_service = 'MyApp::CustomWebhookHandler'