Skip to main content

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

Payment Architecture Diagram

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

Session-based vs direct payment methods

Payment methods indicate whether they use the modern session-based flow via the session_required? method: 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.

Direct payment methods (manual and 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
  • 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:
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. Once the money actually arrives — the cheque clears, the transfer lands, the driver is paid — staff capture the payment from the dashboard, or you do it through the Admin API:
Admin SDK

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

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

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

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

Complete Order

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.This separation ensures the same flow works for all payment types — inline cards, offsite redirects, and wallet payments.

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:

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: 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:
1

List payment methods

The frontend reads the cart’s embedded payment_methods (returned by GET /carts/:id) and checks the session_required flag on each method. Methods with session_required: false use this direct flow.
2

Create payment

The frontend calls POST /payments with the payment_method_id. Spree creates a payment with status checkout. No provider is contacted.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.
3

Complete order

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.

Payment Session

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.

Attributes

A session also carries order_id, but during checkout there is no order yet — it reports the cart’s ID until the cart completes. Read cart_id while paying, and order_id once you have an order.

States

There is no cancel endpoint. A session reaches canceled when the provider says so through its webhook, and expired when it passes expires_at. Neither is something a storefront drives.

API

Create a Payment Session:
Response shape (StorePaymentSession):
Response
Update a Payment Session (e.g., after order total changes):
Complete a Payment Session (after customer confirms payment on the frontend):
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.
Complete the Order (after the payment session is completed):

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:
server/app/models/my_gateway.rb
Supported actions: :captured, :authorized, :failed, :canceled.

Payment

Once a payment session completes, Spree creates a payment to record the result, linked to the session by response_code matching its external_id.

Attributes

These are the fields the Store API returns:
source_type is a plain name — credit_card, not a class name — so you can switch on it directly.

Payment statuses

When the money is taken follows the store’s capture_method, which a payment method can override for its own payments: The last two leave the payment at pending until capture. See Configuration for where the store setting lives.
There is no state machine behind these. A payment’s status is written by the workflows that process, capture and void it, so nothing transitions on its own and there is no sequence a client has to drive.

Order Payment Status

Each payment update also recalculates the order’s payment_status, derived from its payments and refunds against the order total:
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.

Refunds

Refunds are an Admin API operation — there is no Store API route, so a customer cannot start one from your storefront:
Admin SDK
A refund publishes payment.refunded and moves the order’s payment_status to partially_refunded or refunded.

Payment Sources

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

Saved cards

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

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

Maps a Spree customer to their provider-specific customer profile. This enables features like saved payment methods, recurring billing, and customer-level fraud detection. A customer has at most one record per payment method, and profile_id is encrypted with Active Record encryption where it is configured.
This one is internal — the API does not expose it. It is listed here because gateway integrations rely on it; nothing a storefront does will read it.

Payment Setup Sessions

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.

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

Payment Setup Session Attributes

Payment Setup Session API

Payment Setup Sessions require customer authentication. The customer must be logged in.
Create a Payment Setup Session:
Response shape (StorePaymentSetupSession):
Response
Get a Payment Setup Session:
Complete a Payment Setup Session (after the customer completes setup on the frontend using the gateway SDK and external_client_secret):
Spree verifies the result with the provider and saves a payment source — usually a card — 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.

Stripe

Stripe integration, supports all Stripe payment methods, including credit cards, bank transfers, Apple Pay, Google Pay, Klarna, Afterpay, and more. Also supports quick checkout.

Adyen

Adyen integration, supports all Adyen payment methods, including credit cards, bank transfers, Apple Pay, Google Pay, Klarna, and more.

PayPal

Native PayPal integration, supports PayPal, PayPal Credit, and PayPal Pay Later.

Payment Events

Spree publishes events throughout the payment lifecycle that you can subscribe to. For the delivered payload schemas of these events (e.g. payment.paid, payment_session.completed), see the Webhooks & Events reference:

Payment Events

Payment Session Events

Payment Setup Session Events

See Events for more details on subscribing to events.

Two paths to a completed order

A payment can finish in two places, and both have to end at the same result. The customer’s browser confirms the payment and your storefront completes the cart. Or the provider’s own webhook arrives first — sometimes seconds later, sometimes because the customer closed the tab mid-redirect. Whichever arrives first completes the order; the other finds the work already done and does nothing. That is what makes a closed tab or a flaky connection recoverable rather than a lost sale with a real charge attached.
Never treat the browser returning from a redirect as proof of payment. The provider’s webhook is the authoritative signal — a customer can close the tab, and a browser response can be forged.
Both paths can be replaced if your integration needs different behaviour — see Dependencies.