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:

  1. A checkout session — the active payment window with a finite lifetime
  2. 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 naturally

Entry points

Checkout can be initiated in several ways. All of them produce the same result: a checkout session tied to an order.

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 expiresInMinutes at 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_a1b2c3d4e5f6

The 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/unpaid

There 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)
SettingOrgLinkSession
currencyDefaultOverride
successUrlDefaultOverrideOverride
callbackUrlDefaultOverrideOverride
termsUrlSet here
privacyUrlSet here
supportEmailSet 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:

FieldWhat it collects
phoneCustomer phone number
locationCountry and postal code only — minimal data for compliance screening
shippingAddressFull 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.