# Checkout Flow

> Understand the complete data flow from checkout session creation through payment confirmation. Table relationships, status transitions, and every database write explained.

Source: https://www.satsrail.com/developers/guides/checkout-flow/

How data moves from session creation to payment confirmation.

## Table Relationships

```
Merchant
   │ has_many
   ▼
CheckoutSession ──belongs_to──▶ Order ──has_one──▶ Invoice ──has_one──▶ Payment
   │                              │                   │                   │
   │ token                        │ order_number      │ payment_request   │ preimage
   │ amount_cents                 │ total_amount_sats │ payment_hash      │ amount_sats
   │ customer_*                   │ items (jsonb)     │                   │ btc_fiat_price
   │ payment_method               │ metadata (jsonb)  │                   │ fiat_variance
   │ success_url / cancel_url     │                   │                   │ confirmed_at
   │ expires_at (15 min)          │                   │ expires_at        │
   │                              │                   │                   │
   └─ UI session (disposable)     └─ Business record  └─ Network request  └─ Settlement proof
                                     (permanent)        (per attempt)       (immutable)
```

## Why Four Tables?

Each table captures a distinct stage with a different lifecycle:

| Table | Purpose | Lifecycle |
| --- | --- | --- |
| **CheckoutSession** | Temporary UI session. Captures customer intent and info before any money is involved. Expires in 15 minutes. | Disposable |
| **Order** | Permanent business record. The merchant's receipt — tied to items, amounts, and a wallet. Feeds reports, dashboards, and refunds. | Permanent |
| **Invoice** | Payment-network request. Holds the BOLT-11 string (Lightning) plus confirmation tracking. One Order may need a new Invoice if the first expires. | Per attempt |
| **Payment** | Proof of settlement. Records the exact BTC/fiat price at confirmation, the preimage, and calculates price variance. | Immutable |

> **Key insight:** Separating Invoice from Order allows a single Order to survive invoice expiration. If a Lightning invoice expires after 1 hour, a new Invoice can be generated for the same Order with an updated BTC price — without losing the order context.

## Entry Points

Three ways to start a checkout, one unified flow:

###### API

`POST /api/v1/checkout_sessions`

Server-side integration. Returns checkout_url to redirect customer.

###### Payment Link

`GET /pay/:slug`

Zero-code. Share a URL via email, social media, or QR code.

###### Embed Button

`data-key="pk_live_..."`

JavaScript widget. Opens checkout overlay from any webpage.

All three converge at the same CheckoutController, entering the 3-step pipeline below.

## Step-by-Step Data Flow

##### 1. Session Created

API, payment link, or embed widget creates a CheckoutSession. Customer is redirected to the checkout page.

WRITE: `checkout_sessions`

token, amount_cents, currency, merchant_id, product_id, success_url, cancel_url, metadata, expires_at

##### 2. Collect Customer Info optional

If the merchant requires customer info collection and it was not pre-filled via API, the customer fills out a form.

WRITE: `checkout_sessions`

customer_email, customer_name, customer_phone, customer_address

##### 4. Order + Invoice Created

On the first visit to the QR code page, the system creates an Order, links it to the session, converts the fiat amount to sats at the current BTC price, and generates an Invoice with a BOLT-11 payment request.

WRITE: `orders`

merchant_id, wallet_id, total_amount_cents, total_amount_sats, items, metadata, status = pending

WRITE: `checkout_sessions`

order_id = new order

WRITE: `invoices`

payment_request, payment_hash, amount_sats, expires_at

##### 5. QR Code + Monitoring

The checkout page displays a QR code. Background jobs begin polling for payment:

PaymentMonitorJob polls every 5 seconds (up to 15 minutes).

The checkout page listens on WebSocket (ActionCable) for instant updates, with HTTP polling every 5 seconds as fallback.

##### Payment Confirmed

When a background job detects settlement:

WRITE: `invoices`

status = paid, paid_at

WRITE: `orders`

status = paid

WRITE: `payments`

amount_sats, preimage, btc_fiat_price, fiat_value, fiat_variance, confirmed_at

WRITE: `checkout_sessions`

status = completed

WebSocket broadcasts to checkout page, which redirects the customer to success_url. Webhooks fire asynchronously.

##### Session Expired (no payment)

If 15 minutes pass with no payment detected:

WRITE: `invoices`

status = expired

WRITE: `checkout_sessions`

status = expired

Customer is redirected to cancel_url.

## Bitcoin Conversion Fee

A merchant can add a fee of up to 3% to bitcoin payments to cover selling the sats for local currency. The price itself (`amount_cents`, `total_amount_cents`) never changes: the fee is charged on top of it and the invoice is priced at `amount_due_cents`. The hosted checkout shows it as its own line. When the fee is not zero, these fields are added so your own UI can show it too:

```
"total_amount_cents": 10000,
"conversion_fee_percent": 1.5,
"conversion_fee_cents": 150,
"amount_due_cents": 10150,
"total_amount_sats": 101500   // priced on amount_due_cents
```

Returned on orders, invoices, payment requests, checkout sessions, the checkout status poll and the order in webhook payloads. The fields are absent when the fee is zero, and on cash orders, which never carry it. A new checkout session previews the merchant's current rate; once the order exists, its own recorded rate is what the customer is charged. `GET /api/v1/m/merchant` returns the configured `conversion_fee_percent` for clients that build their own orders.

## Status Transitions

###### CheckoutSession

```
pending ──▶ completed  (paid)
pending ──▶ expired    (15 min)
pending ──▶ cancelled
```

###### Order

```
pending ──▶ invoice_generated
        ──▶ paid
        ──▶ cancelled
paid    ──▶ refunded
```

###### Invoice

```
pending ──▶ paid      (settled)
pending ──▶ expired   (TTL)
pending ──▶ cancelled
```

###### Payment

```
No status enum.
Created once on confirmation.
Immutable after creation.

Proof: preimage (Lightning)
```

## Every Database Write, In Order

| # | Trigger | Table | Operation |
| --- | --- | --- | --- |
| 1 | API call / payment link / embed | `checkout_sessions` | INSERT token, amount, currency, merchant, product, urls, metadata, expires_at |
| 2 | Customer submits info form | `checkout_sessions` | UPDATE customer_email, customer_name, customer_phone, customer_address |
| 3 | First visit to QR page | `orders` | INSERT merchant, wallet, amounts, items, metadata |
| 5 | First visit to QR page | `checkout_sessions` | UPDATE order_id = new order |
| 6 | Invoice generation | `orders` | UPDATE total_amount_sats (fresh BTC price) |
| 7 | Invoice generation | `invoices` | INSERT payment_request, payment_hash, amount_sats, expires_at |
| 8 | Background job detects payment | `invoices` | UPDATE status = paid, paid_at |
| 9 | Background job detects payment | `orders` | UPDATE status = paid |
| 10 | Background job detects payment | `payments` | INSERT amount_sats, preimage, btc_fiat_price, fiat_value, fiat_variance, confirmed_at |
| 11 | Background job detects payment | `checkout_sessions` | UPDATE status = completed |

## Ready to integrate?

Create a checkout session with a single API call.

[Checkout API →](https://www.satsrail.com/developers/) [Embed Button →](https://www.satsrail.com/developers/guides/embed/)
