Packages

Checkout

The CheckoutSession state machine — manages the checkout flow from customer info to payment confirmation, across web, POS, and AI channels.

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

ChannelDefault FulfillmentUse Case
webshippingStandard e-commerce
posnoneIn-store terminal
agentnoneAI / chatbot
linknonePayment links, QR codes

Fulfillment Types

FulfillmentRequires AddressUse Case
shippingShip to customer
local_deliveryFood delivery, courier
pickupDine-in, click & collect
noneDigital goods, QR payment

Configuration

The CheckoutSessionConfig accepts these options:

OptionTypeRequiredDescription
paymentProviderPaymentProviderYesPayment gateway instance
amountnumberYesOrder total
currencystringYesISO currency code
channelCheckoutChannelNoSales channel (default: 'web')
fulfillmentCheckoutFulfillmentNoFulfillment type (smart default from channel)
expiresInnumberNoSession TTL in milliseconds
orderIdstringNoExternal order reference
returnUrlstringNo3DS redirect return URL
cancelUrlstringNoPayment cancellation URL
webhookUrlstringNoPer-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: requires shipping state
  • pickup / none: can submit from info state (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):

FromToTrigger
idleinfosetCustomerInfo()
infoshippingsetShippingAddress()
shippingpaymentsubmitPayment()
paymentconfirmingconfirmPayment()
confirmingcompletePayment captured
confirmingfailedPayment declined
failedpaymentRetry submitPayment()

Without address (pickup, none):

FromToTrigger
idleinfosetCustomerInfo()
infopaymentsubmitPayment()
paymentconfirmingconfirmPayment()
confirmingcompletePayment captured
confirmingfailedPayment declined
failedpaymentRetry submitPayment()
Calling a method that requires a different state throws an error. For example, calling submitPayment() while in idle state results in Error: Cannot submit payment in "idle" state.