Skip to main content

Building a Gateway Provider

This guide walks through creating a new payment gateway provider package. The same pattern applies to shipping carriers — substitute GatewayAdapter with ShippingAdapter and adjust the hub/category accordingly.

1. Scaffold the package​

Create a new workspace package:

packages/gateway-myprovider/
├── package.json
├── tsconfig.json
└── src/
└── modules/
└── gateway_myprovider/
├── index.ts
├── integration.ts
├── acl.ts
├── setup.ts
├── di.ts
├── lib/
│ ├── adapter.ts
│ ├── webhook-handler.ts
│ ├── status-map.ts
│ └── health.ts
├── widgets/
│ └── payments/
│ └── client.tsx
├── workers/
│ └── webhook-processor.ts
└── i18n/
└── en.ts

Add the package to apps/saasframe/src/modules.ts to enable it.

2. Define the integration manifest​

Declare metadata in integration.ts:

import type { IntegrationDefinition } from '@saasframe/shared/modules/integrations/types'

export const integration: IntegrationDefinition = {
id: 'gateway_myprovider',
title: 'My Provider',
description: 'Accept payments via My Provider.',
category: 'payment',
hub: 'payment_gateways',
providerKey: 'myprovider',
icon: 'myprovider',
package: '@saasframe/gateway-myprovider',
version: '1.0.0',
author: 'Your Name',
company: 'My Provider, Inc.',
credentials: {
fields: [
{ key: 'apiKey', label: 'API Key', type: 'secret', required: true },
{ key: 'webhookSecret', label: 'Webhook Secret', type: 'secret', required: true },
],
},
healthCheck: { service: 'myProviderHealthCheck' },
}

export const integrations: IntegrationDefinition[] = [integration]
export const bundles = []

This registers the provider in the Integration Marketplace with a credential form and health check.

3. Implement the adapter​

Create lib/adapter.ts:

import type { GatewayAdapter, CreateSessionInput, CreateSessionResult } from '@saasframe/shared/modules/payment_gateways/types'
import { mapMyProviderStatus } from './status-map'

export const myProviderAdapter: GatewayAdapter = {
providerKey: 'myprovider',

async createSession(input: CreateSessionInput): Promise<CreateSessionResult> {
const client = createClient(input.credentials)
const session = await client.createPayment({
amount: input.amount,
currency: input.currencyCode,
})
return {
sessionId: session.id,
redirectUrl: session.checkoutUrl,
status: mapMyProviderStatus(session.status),
}
},

async capture(input) {
const client = createClient(input.credentials)
const result = await client.capture(input.sessionId, input.amount)
return { status: mapMyProviderStatus(result.status), capturedAmount: result.amount }
},

async refund(input) {
const client = createClient(input.credentials)
const result = await client.refund(input.sessionId, input.amount, input.reason)
return { refundId: result.id, status: mapMyProviderStatus(result.status), refundedAmount: result.amount }
},

async cancel(input) {
const client = createClient(input.credentials)
await client.cancel(input.sessionId)
return { status: 'cancelled' }
},

async getStatus(input) {
const client = createClient(input.credentials)
const payment = await client.getPayment(input.sessionId)
return {
status: mapMyProviderStatus(payment.status),
amount: payment.amount,
amountReceived: payment.captured,
currencyCode: payment.currency,
}
},

async verifyWebhook(input) {
const secret = input.credentials.webhookSecret as string
// Verify signature using provider SDK
const event = verifySignature(input.rawBody, input.headers, secret)
return {
eventType: event.type,
eventId: event.id,
data: event.data,
idempotencyKey: event.id,
timestamp: new Date(event.created),
}
},

mapStatus: mapMyProviderStatus,
}

4. Build the status map​

Create lib/status-map.ts:

import type { UnifiedPaymentStatus } from '@saasframe/shared/modules/payment_gateways/types'

const STATUS_MAP: Record<string, UnifiedPaymentStatus> = {
'created': 'pending',
'awaiting_payment': 'pending',
'authorized': 'authorized',
'paid': 'captured',
'refunded': 'refunded',
'cancelled': 'cancelled',
'failed': 'failed',
'expired': 'expired',
}

export function mapMyProviderStatus(providerStatus: string): UnifiedPaymentStatus {
return STATUS_MAP[providerStatus] ?? 'unknown'
}

5. Register at runtime in di.ts​

import type { AppContainer } from '@saasframe/shared/lib/di/container'
import { registerGatewayAdapter, registerWebhookHandler } from '@saasframe/shared/modules/payment_gateways/types'
import { myProviderAdapter } from './lib/adapter'
import { verifyMyProviderWebhook } from './lib/webhook-handler'

export function register(container: AppContainer) {
registerGatewayAdapter(myProviderAdapter)
registerWebhookHandler('myprovider', verifyMyProviderWebhook, {
queue: 'myprovider-webhook',
})
}

Keep setup.ts for tenant initialization concerns such as defaultRoleFeatures, seeded configuration, example data, and provider-owned env-backed preconfiguration that persists credentials or defaults. Do not rely on setup.ts for runtime adapter registration, because the gateway registry is in-memory and must be populated during app boot.

Recommended provider pattern:

  • read env vars in a provider-local helper such as lib/preset.ts
  • apply them from setup.ts to persist credentials/defaults after fresh install
  • expose a provider CLI command such as configure-from-env so operators can rerun the preset later
  • keep the preset logic inside the provider package instead of adding provider-specific code to core

6. Add the webhook worker​

Create workers/webhook-processor.ts:

import type { Job } from '@saasframe/queue'
import type { WorkerContext, WorkerMeta } from '@saasframe/shared/modules/registry'

export const metadata: WorkerMeta = {
queue: 'myprovider-webhook',
id: 'myprovider-webhook-processor',
concurrency: 5,
}

export default async function handler(job: Job, ctx: WorkerContext) {
const { event, scope, transactionId } = job.data
// Update transaction status, emit domain events, log activity
}

7. Add health check​

Create lib/health.ts and register in di.ts:

export function createMyProviderHealthCheck(deps) {
return {
async check(credentials: Record<string, unknown>) {
const client = createClient(credentials)
await client.ping()
return { healthy: true }
},
}
}

8. Configure webhooks​

Tell the payment provider to send events to:

{YOUR_APP_URL}/api/payment_gateways/webhook/myprovider

The dynamic [provider] route automatically dispatches to your registered handler.

For local development, expose your app publicly with a tunnel such as ngrok:

ngrok http 3000

Then configure the provider to send webhooks to:

https://YOUR-NGROK-SUBDOMAIN.ngrok-free.app/api/payment_gateways/webhook/myprovider

9. Enable and test​

  1. Add the package to apps/saasframe/src/modules.ts.
  2. Run yarn generate and yarn build:packages.
  3. Go to Settings > Integrations, find your provider, and enter credentials.
  4. Use the Payment Gateway Demo page or create a payment method with your provider key.

Provider-owned payment widgets​

If your provider supports inline payment UI, register it from widgets/payments/client.tsx. That file is auto-discovered by the generator and imported through payments.client.generated.ts during client bootstrap.

Use this file to register renderer widgets declaratively by providerKey + rendererKey. Consumer modules such as checkout should only resolve them through the shared payment renderer registry; they must not import provider UI directly.

When a host page exposes payment-widget injection spots, keep provider-specific embellishments in those widgets too. Validation and submit hooks should flow through UMES behavior spots instead of custom provider branches.

Shipping carrier providers​

For shipping carriers, follow the same pattern with these differences:

PaymentShipping
GatewayAdapterShippingAdapter
hub: 'payment_gateways'hub: 'shipping_carriers'
category: 'payment'category: 'shipping'
registerGatewayAdapter()registerShippingAdapter()
Methods: createSession, capture, refund, cancelMethods: calculateRates, createShipment, getTracking, cancelShipment
Webhook route: /api/payment_gateways/webhook/{provider}Webhook route: /api/shipping_carriers/webhook/{provider}

Stripe reference​

The @saasframe/gateway-stripe package is the reference implementation. It demonstrates:

  • Versioned adapters (supporting multiple Stripe API versions)
  • Webhook signature verification using the Stripe SDK
  • Status mapping for Payment Intents and Charges
  • Health check via stripe.accounts.retrieve()
  • Admin UI widget injection for capture mode configuration

See the Stripe configuration guide for setup instructions.