Checkout Sessions
How checkout sessions work, the settings hierarchy, entry points, and session management.
Overview
Checkout sessions are the intent system that takes a customer from "I want to buy this" to "payment complete." It sits between the entry point — a payment link, a POS terminal, or a direct API call — and the order that records the purchase.
Every checkout session creates two things:
- A checkout session — the active payment window with a finite lifetime
- An order — the cart that tracks what's being purchased and whether it's been paid
The session is ephemeral (it expires); the order is permanent (it becomes the receipt).
Entry point Checkout system Result
───────────── ─────────────── ──────
Payment link URL ─┐
├──→ Checkout session created ──→ Order (open, unpaid)
Programmatic API ──┤ (60m default window) │
│ │
POS (future) ──┘ Customer pays ▼
│ Order (completed, paid)
▼
Session expires naturallyEntry points
Checkout can be initiated in several ways. All of them produce the same result: a checkout session tied to an order.
Payment links
The most common path. A merchant creates a payment link, shares the URL, and the customer opens it. A session and order are created automatically — no direct API calls needed.
Programmatic API
For server-side integrations, create a session explicitly:
POST /v0/checkout/sessions{
"items": [{ "name": "Custom Widget", "quantity": 3, "unitPrice": 2500 }],
"currency": "USD",
"customerEmail": "alice@example.com",
"successUrl": "https://example.com/thanks"
}Order details are provided inline — no payment link required. Use this when building custom checkout
flows where you control the order details server-side (like in your website, mobile app, or custom
POS platform). When a customer opens the session.url for Decal's hosted checkout page, they are
able to view and pay for the underlying order.
Checkout sessions
A checkout session is the active payment window. It represents a single customer's attempt to complete a purchase.
What a session is
- Tied to exactly one order, created together in a single database transaction
- Has a configurable expiration window — 60 minutes by default, configurable from 15 minutes up
to 1440 minutes (24 hours) via
expiresInMinutesat session creation - Does not track payment status — the associated order does
After payment completes, the order moves to paid. The session simply expires naturally — its job
is done once the payment flow finishes (or the customer abandons it).
Session IDs
Sessions use the typed ID format cs_{mode}_{id}:
cs_live_x9y8z7w6v5u4
cs_test_a1b2c3d4e5f6The cs_ prefix identifies the resource as a checkout session. The mode (live or test) is
embedded in the ID, preventing cross-environment usage.
Lifecycle
Session created (expiresAt = now + expiresInMinutes, default 60m)
│
▼
Session active ◄──── customer retries payment (failedAttempts++)
│
├──── payment succeeds → order marked paid
│ session expires naturally
│
└──── expiry reached → session inactive
order remains open/unpaidThere is no explicit "completed" status on the session. A session is active if expiresAt > now,
inactive otherwise. The order's paymentStatus is the source of truth for whether checkout
succeeded.
Failed attempts
failedAttempts tracks how many payment attempts failed within this session. Useful for:
- Fraud detection — flag sessions with unusually high failure counts
- UX decisions — show "try a different payment method" after repeated failures
Decal does not enforce a hard limit on retries. Applications implement their own policies based on this counter.
The settings hierarchy
Checkout settings cascade through three layers. Each layer can override the one above it; the most specific non-null value wins.
Organization settings ← org-wide defaults
↓ overridden by
Payment link ← per-link overrides
↓ overridden by
Checkout session ← per-session overrides (programmatic use)| Setting | Org | Link | Session |
|---|---|---|---|
currency | Default | Override | — |
successUrl | Default | Override | Override |
callbackUrl | Default | Override | Override |
termsUrl | Set here | — | — |
privacyUrl | Set here | — | — |
supportEmail | Set here | — | — |
Organization settings establish defaults that apply to all checkouts. termsUrl, privacyUrl,
and supportEmail are org-only — they represent brand-level configuration that doesn't vary per
link or session.
Payment link overrides customize settings for a specific payment page. A link can set its own
successUrl to redirect to a product-specific confirmation page, overriding the org default.
Session overrides are the highest priority. Use these for dynamic per-customer redirects — for example, appending the order ID to the success URL so the merchant's site can display a personalized confirmation page.
Required fields
Required fields use additive OR-merge across layers. A field required at any layer stays required — sessions can add requirements but never remove ones set by the payment link.
Payment link requires: { phone: true }
Session adds: { location: true }
Effective requirements: { phone: true, location: true }If the payment link requires phone, no session can make it optional. This ensures merchants can
enforce minimum data collection regardless of how the session is created.
The available required fields are:
| Field | What it collects |
|---|---|
phone | Customer phone number |
location | Country and postal code only — minimal data for compliance screening |
shippingAddress | Full shipping address (name, street, city, state, postal code, country) |
Managing sessions
Extending a session
To extend a session's expiry:
PATCH /v0/checkout/sessions/{sessionId}{
"extendExpiry": 60
}The extendExpiry value is in minutes and is added to the session's current expiresAt. Use case:
customer support interventions or long checkout flows where the default window isn't enough.