Guides

Payment Integration

How to add a new payment provider to CommerceJS.

CommerceJS uses a pluggable payment architecture. Adding a new provider — Stripe, PayPal, or a custom gateway — requires implementing the PaymentProvider interface from @commercejs/types.

The PaymentProvider Interface

Every payment provider implements four methods:

interface PaymentProvider {
  createSession(input: CreatePaymentSessionInput): Promise<PaymentSession>
  confirmSession(sessionId: string): Promise<PaymentSession>
  refund(input: RefundInput): Promise<PaymentSession>
  verifyWebhook(event: PaymentWebhookEvent): Promise<boolean>
}

Step-by-Step Guide

Create the package

Create a new package in the monorepo:

mkdir -p packages/payment-stripe/src
packages/payment-stripe/package.json
{
  "name": "@commercejs/payment-stripe",
  "version": "0.1.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "dependencies": {
    "@commercejs/types": "workspace:*"
  }
}

Implement the provider

Create the provider class that implements PaymentProvider:

packages/payment-stripe/src/stripe-provider.ts
import type {
  PaymentProvider,
  PaymentSession,
  CreatePaymentSessionInput,
  RefundInput,
  PaymentWebhookEvent,
} from '@commercejs/types'

export class StripePaymentProvider implements PaymentProvider {
  private secretKey: string

  constructor(config: { secretKey: string }) {
    this.secretKey = config.secretKey
  }

  async createSession(input: CreatePaymentSessionInput): Promise<PaymentSession> {
    // Call Stripe's API to create a PaymentIntent
    const response = await fetch('https://api.stripe.com/v1/payment_intents', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.secretKey}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        amount: String(Math.round(input.amount * 100)),
        currency: input.currency.toLowerCase(),
        'payment_method': input.sourceToken!,
        confirm: 'true',
        'return_url': input.returnUrl!,
      }),
    })

    const intent = await response.json()

    return {
      id: intent.id,
      providerId: 'stripe',
      status: this.mapStatus(intent.status),
      amount: input.amount,
      currency: input.currency,
      redirectUrl: intent.next_action?.redirect_to_url?.url ?? null,
      createdAt: new Date().toISOString(),
    }
  }

  async confirmSession(sessionId: string): Promise<PaymentSession> {
    // Retrieve the PaymentIntent to check its status
    const response = await fetch(
      `https://api.stripe.com/v1/payment_intents/${sessionId}`,
      { headers: { 'Authorization': `Bearer ${this.secretKey}` } },
    )
    const intent = await response.json()

    return {
      id: intent.id,
      providerId: 'stripe',
      status: this.mapStatus(intent.status),
      amount: intent.amount / 100,
      currency: intent.currency.toUpperCase(),
      redirectUrl: null,
      createdAt: new Date(intent.created * 1000).toISOString(),
    }
  }

  async refund(input: RefundInput): Promise<PaymentSession> {
    // Create a Stripe refund
    throw new Error('Not implemented')
  }

  async verifyWebhook(event: PaymentWebhookEvent): Promise<boolean> {
    // Verify Stripe webhook signature
    throw new Error('Not implemented')
  }

  private mapStatus(stripeStatus: string): PaymentSession['status'] {
    switch (stripeStatus) {
      case 'succeeded': return 'captured'
      case 'requires_action': return 'requires_action'
      case 'requires_payment_method': return 'failed'
      case 'canceled': return 'cancelled'
      case 'processing': return 'processing'
      default: return 'pending'
    }
  }
}

Export the provider

packages/payment-stripe/src/index.ts
export { StripePaymentProvider } from './stripe-provider.js'

Use it with CheckoutSession

import { CheckoutSession } from '@commercejs/checkout'
import { StripePaymentProvider } from '@commercejs/payment-stripe'

const session = new CheckoutSession({
  provider: new StripePaymentProvider({
    secretKey: process.env.STRIPE_SECRET_KEY!,
  }),
  amount: 49.99,
  currency: 'USD',
})

Status Mapping

Map your provider's statuses to PaymentSessionStatus:

PaymentSessionStatusMeaning
pendingPayment created, not yet processed
processingPayment is being processed
requires_actionCustomer action needed (3DS, redirect)
capturedPayment captured successfully
failedPayment failed or declined
cancelledPayment cancelled
refundedPayment refunded

Webhook Verification

Add a webhook verifier config for your provider:

packages/payment-stripe/src/webhook-config.ts
export const stripeWebhookConfig = {
  formatter: (body: any) => `${body.id}.${body.created}`,
  algorithm: 'hmac-sha256' as const,
  getExpectedSignature: (headers: Record<string, string>) => {
    const sigHeader = headers['stripe-signature'] ?? ''
    const parts = sigHeader.split(',')
    const sig = parts.find(p => p.startsWith('v1='))
    return sig?.replace('v1=', '') ?? null
  },
}
See @commercejs/payment-tap for a complete, production-ready implementation to use as a reference.