Checkout
The @commercejs/checkout package provides the CheckoutSession class — a framework-agnostic state machine that orchestrates the complete checkout lifecycle. It supports multiple sales channels and fulfillment types, dynamically adjusting the checkout flow.
Installation
pnpm add @commercejs/checkout
Basic Usage
import { CheckoutSession } from '@commercejs/checkout'
// Standard e-commerce checkout
const session = new CheckoutSession({
paymentProvider: myPaymentProvider,
amount: 49.99,
currency: 'BHD',
orderId: 'order-123',
returnUrl: 'https://myshop.com/checkout/confirm',
webhookUrl: 'https://myshop.com/api/webhooks/payment',
})
Channel-Agnostic Checkout
The session adapts its flow based on the channel and fulfillment type:
// POS terminal — no address needed
const posSession = new CheckoutSession({
paymentProvider: tap,
amount: 45.00,
currency: 'SAR',
channel: 'pos', // defaults fulfillment to 'none'
})
// Restaurant with delivery
const deliverySession = new CheckoutSession({
paymentProvider: tap,
amount: 85.00,
currency: 'SAR',
channel: 'link',
fulfillment: 'local_delivery', // requires address
})
Channels
| Channel | Default Fulfillment | Use Case |
|---|---|---|
web | shipping | Standard e-commerce |
pos | none | In-store terminal |
agent | none | AI / chatbot |
link | none | Payment links, QR codes |
Fulfillment Types
| Fulfillment | Requires Address | Use Case |
|---|---|---|
shipping | ✅ | Ship to customer |
local_delivery | ✅ | Food delivery, courier |
pickup | ❌ | Dine-in, click & collect |
none | ❌ | Digital goods, QR payment |
Configuration
The CheckoutSessionConfig accepts these options:
| Option | Type | Required | Description |
|---|---|---|---|
paymentProvider | PaymentProvider | Yes | Payment gateway instance |
amount | number | Yes | Order total |
currency | string | Yes | ISO currency code |
channel | CheckoutChannel | No | Sales channel (default: 'web') |
fulfillment | CheckoutFulfillment | No | Fulfillment type (smart default from channel) |
expiresIn | number | No | Session TTL in milliseconds |
orderId | string | No | External order reference |
returnUrl | string | No | 3DS redirect return URL |
cancelUrl | string | No | Payment cancellation URL |
webhookUrl | string | No | Per-transaction webhook URL |
Methods
setCustomerInfo(info)
Set customer details and transition from idle to info:
session.setCustomerInfo({
email: 'ahmed@example.com',
firstName: 'Ahmed',
lastName: 'Al-Rashid',
phone: '+97312345678',
})
setShippingAddress(address, billingAddress?)
Set shipping address (and optional billing address) to transition from info to shipping. Only required when fulfillment is shipping or local_delivery:
session.setShippingAddress({
street: '123 King Faisal Highway',
street2: null,
city: 'Manama',
state: null,
country: 'BH',
postalCode: '1234',
district: null,
nationalAddress: null,
})
setShippingMethod(methodId)
Set the shipping method. Does not trigger a state transition — call submitPayment() when ready:
session.setShippingMethod('standard-shipping')
submitPayment(options?)
Create a payment session and transition to payment. The valid source state depends on fulfillment type:
shipping/local_delivery: requiresshippingstatepickup/none: can submit frominfostate (skips address step)
const paymentSession = await session.submitPayment({
sourceToken: 'tok_xxx', // Tokenized card
idempotencyKey: 'key-123', // Prevent duplicate charges
metadata: { source: 'web' }, // Extra data
})
Returns a PaymentSession with an optional redirectUrl for 3DS.
confirmPayment(sessionId?)
Confirm payment after 3DS redirect. Transitions from payment to complete or failed:
const confirmed = await session.confirmPayment('chg_abc123')
handleWebhookUpdate(paymentSession)
Handle async webhook events. Works from any non-terminal state:
session.handleWebhookUpdate({
id: 'chg_xxx',
providerId: 'tap',
status: 'captured',
amount: 49.99,
currency: 'BHD',
redirectUrl: null,
createdAt: '2026-02-10T00:00:00Z',
})
toSnapshot()
Get a serializable snapshot for SSR hydration or API responses:
const snapshot = session.toSnapshot()
// Includes: state, channel, fulfillment, expiresAt,
// customerInfo, shippingAddress, paymentSession, etc.
Session Expiry
Sessions can be given a time-to-live. Expired sessions reject all state-mutating calls:
const session = new CheckoutSession({
paymentProvider: tap,
amount: 45.00,
currency: 'SAR',
channel: 'pos',
expiresIn: 30 * 60 * 1000, // 30 minutes
})
session.on('expired', () => {
console.log('Session expired')
})
Events
session.on('stateChange', ({ from, to }) => { /* ... */ })
session.on('complete', ({ paymentSession }) => { /* ... */ })
session.on('error', ({ error, state }) => { /* ... */ })
session.on('expired', () => { /* ... */ })
State Transitions
The transition map is dynamic based on fulfillment type:
With address (shipping, local_delivery):
| From | To | Trigger |
|---|---|---|
idle | info | setCustomerInfo() |
info | shipping | setShippingAddress() |
shipping | payment | submitPayment() |
payment | confirming | confirmPayment() |
confirming | complete | Payment captured |
confirming | failed | Payment declined |
failed | payment | Retry submitPayment() |
Without address (pickup, none):
| From | To | Trigger |
|---|---|---|
idle | info | setCustomerInfo() |
info | payment | submitPayment() |
payment | confirming | confirmPayment() |
confirming | complete | Payment captured |
confirming | failed | Payment declined |
failed | payment | Retry submitPayment() |
submitPayment() while in idle state results in Error: Cannot submit payment in "idle" state.