How It Works
The payment lifecycle from checkout to cash — big picture first, then details.
Big Picture
Every payment flows through three objects:
| Object | What it is | You create it? |
|---|---|---|
| Checkout Session | A payment page for your customer | Yes — POST /checkout-sessions |
| Payment Intent | Tracks the payment through its lifecycle | Auto-created with the session |
| Charge | The actual money movement (auth, capture, settle) | Auto-created when customer pays |
You only interact with the first one. We handle the rest.
Your Server CX Pay Customer
────────── ───── ────────
POST /checkout-sessions ──▶ Creates CS + PI
◀── { id, url, payment_intent }
Redirect ──────────────────────────────────────────────▶ Sees hosted checkout
Enters card details
3DS challenge (if needed)
Charge created ──────────▶ Payment processed
PI → succeeded
webhook: payment_intent.succeeded ◀──── CX Pay POST to your endpoint
Fulfill the order ✓Payment Intent Lifecycle
The Payment Intent is the source of truth for "did they pay?" Here are the states you'll see:
┌──────────────────┐
│ requires_payment │ ← just created
│ _method │
└────────┬─────────┘
│ customer submits card
▼
┌──────────────────┐
│ processing │ ← payment in flight
└────────┬─────────┘
│
┌────────┴─────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ succeeded │ │ failed │
└──────────────┘ └──────────────┘The states that matter to you:
| Status | What it means | What to do |
|---|---|---|
succeeded | Money captured. Done. | Fulfill the order |
processing | Payment submitted, waiting on network | Wait for webhook |
requires_action | Customer needs to complete 3DS | Nothing — we handle it |
canceled | Customer or session expired | Show "payment canceled" |
expired | Session timed out | Offer to try again |
Payment Intent vs Charge
- Payment Intent = the intention to collect money. Tracks the full lifecycle.
- Charge = the actual money movement. Created when the payment is attempted.
A Payment Intent can have multiple Charges (e.g., if the first attempt fails and the customer retries). You almost never need to think about Charges — just check the Payment Intent status.
Rule of thumb: Check payment_intent.status to know if you got paid. Ignore Charges unless you're debugging.
Checkout Session vs Payment Intent
"When do I use which?"
- Checkout Session = what you create. It's a wrapper that generates a hosted payment page.
- Payment Intent = what you check. The
payment_intentID from the session response is your reference for payment status.
Checkout Session (cs_...)
└── owns → Payment Intent (pi_...)
└── owns → Charge (ch_...)You create the session. You check the intent. You ignore the charge. Simple.