• Blog
  • Documentation
  • Courses
  • Changelog
  • AI Starters
  • UI Kit
  • FAQ
  • Supamode
    New
  • Pricing

Launch your next SaaS in record time with Makerkit, a React SaaS Boilerplate for Next.js and Supabase.

Makerkit is a product of Makerkit Pte Ltd (registered in the Republic of Singapore)Company Registration No: 202407149CFor support or inquiries, please contact us

About
  • FAQ
  • Contact
  • Verify your Discord
  • Consultation
  • Open Source
  • Become an Affiliate
Product
  • Documentation
  • Blog
  • Changelog
  • UI Blocks
  • Figma UI Kit
  • AI SaaS Starters
License
  • Activate License
  • Upgrade License
  • Invite Member
Legal
  • Terms of License
    • How Billing Works
    • Billing Schema
    • Stripe
    • Lemon Squeezy
    • Paddle
    • Metered Usage
    • Per Seat Billing
    • Credits Based Billing
    • One Off Payments
    • Handling Webhooks
    • Billing API
    • Custom Integration

How to create a custom billing integration in Makerkit

Learn how to create a custom billing integration in Makerkit

This guide explains how to create billing integration plugins for the Makerkit SaaS platform to allow you to use a custom billing provider.

How to create a custom billing integration in Makerkit

Learn how to create a custom billing integration in Makerkit

1

Architecture Overview

2

Package Structure

3

Core Interface Implementation

4

Environment Configuration

5

Billing Strategy Service

6

Webhook Handler Service

7

Client-Side Components

8

Registration and Integration

9

Testing Strategy

10

Security Best Practices

11

Example Implementation

Architecture Overview

The Makerkit billing system uses a plugin-based architecture that allows multiple billing providers to coexist. The system consists of:

Core Components

  1. Billing Strategy Provider Service - Abstract interface for billing operations
  2. Billing Webhook Handler Service - Abstract interface for webhook processing
  3. Registry System - Dynamic loading and management of providers
  4. Schema Validation - Type-safe configuration and data validation

Provider Structure

Each billing provider is implemented as a separate package under packages/{provider-name}/ with:

  • Server-side services - Billing operations and webhook handling
  • Client-side components - Checkout flows and UI integration
  • Configuration schemas - Environment variable validation
  • SDK abstractions - Provider-specific API integrations

Data Flow

Client Request → Registry → Provider Service → External API → Webhook → Handler → Database

Creating a package

You can create a new package for your billing provider by running the following command:

pnpm turbo gen package

This will create a new package in the packages directory, ready to use. You can move this anywhere in the packages directory, but we recommend keeping it in the packages/billing directory.

Package Structure

Once we finalize the package structure, your structure should look like this:

packages/{provider-name}/
├── package.json
├── tsconfig.json
├── index.ts
└── src/
├── index.ts
├── components/
│ ├── index.ts
│ └── {provider}-checkout.tsx
├── constants/
│ └── {provider}-events.ts
├── schema/
│ ├── {provider}-client-env.schema.ts
│ └── {provider}-server-env.schema.ts
└── services/
├── {provider}-billing-strategy.service.ts
├── {provider}-webhook-handler.service.ts
├── {provider}-sdk.ts
└── create-{provider}-billing-portal-session.ts

package.json Template

{
"name": "@kit/{provider-name}",
"private": true,
"version": "0.1.0",
"exports": {
".": "./src/index.ts",
"./components": "./src/components/index.ts"
},
"typesVersions": {
"*": {
"*": ["src/*"]
}
},
"dependencies": {
"{provider-sdk}": "^x.x.x"
},
"devDependencies": {
"@kit/billing": "workspace:*",
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/react": "19.1.13",
"next": "15.5.4",
"react": "19.1.1",
"zod": "^3.25.74"
}
}

Core Interface Implementation

BillingStrategyProviderService

This abstract class defines the contract for all billing operations:

packages/{provider}/src/services/{provider}-billing-strategy.service.ts

import { BillingStrategyProviderService } from '@kit/billing';
export class YourProviderBillingStrategyService
implements BillingStrategyProviderService
{
private readonly namespace = 'billing.{provider}';
async createCheckoutSession(params) {
// Implementation
}
async createBillingPortalSession(params) {
// Implementation
}
async cancelSubscription(params) {
// Implementation
}
async retrieveCheckoutSession(params) {
// Implementation
}
async reportUsage(params) {
// Implementation (if supported)
}
async queryUsage(params) {
// Implementation (if supported)
}
async updateSubscriptionItem(params) {
// Implementation
}
async getPlanById(planId: string) {
// Implementation
}
async getSubscription(subscriptionId: string) {
// Implementation
}
}

BillingWebhookHandlerService

This abstract class handles webhook events from the billing provider:

packages/{provider}/src/services/{provider}-webhook-handler.service.ts

import { BillingWebhookHandlerService } from '@kit/billing';
export class YourProviderWebhookHandlerService
implements BillingWebhookHandlerService
{
private readonly provider = '{provider}' as const;
private readonly namespace = 'billing.{provider}';
async verifyWebhookSignature(request: Request) {
// Verify signature using provider's SDK
// Throw error if invalid
}
async handleWebhookEvent(event: unknown, params) {
// Route events to appropriate handlers
switch (event.type) {
case 'subscription.created':
return this.handleSubscriptionCreated(event, params);
case 'subscription.updated':
return this.handleSubscriptionUpdated(event, params);
// ... other events
}
}
}

Environment Configuration

Server Environment Schema

Create schemas for server-side configuration:

packages/{provider}/src/schema/{provider}-server-env.schema.ts

// src/schema/{provider}-server-env.schema.ts
import { z } from 'zod';
export const YourProviderServerEnvSchema = z.object({
apiKey: z.string({
description: '{Provider} API key for server-side operations',
required_error: '{PROVIDER}_API_KEY is required',
}),
webhooksSecret: z.string({
description: '{Provider} webhook secret for verifying signatures',
required_error: '{PROVIDER}_WEBHOOK_SECRET is required',
}),
});
export type YourProviderServerEnv = z.infer<typeof YourProviderServerEnvSchema>;

Client Environment Schema

Create schemas for client-side configuration:

packages/{provider}/src/schema/{provider}-client-env.schema.ts

// src/schema/{provider}-client-env.schema.ts
import { z } from 'zod';
export const YourProviderClientEnvSchema = z.object({
publicKey: z.string({
description: '{Provider} public key for client-side operations',
required_error: 'NEXT_PUBLIC_{PROVIDER}_PUBLIC_KEY is required',
}),
});
export type YourProviderClientEnv = z.infer<typeof YourProviderClientEnvSchema>;

Billing Strategy Service

Implementation Example

This is an abstract example

The "client" class in the example below is not a real class, it's just an example of how to implement the BillingStrategyProviderService interface. You should refer to the SDK of your billing provider to implement the actual methods.

Here's a detailed implementation pattern based on the Paddle service:

import 'server-only';
import { z } from 'zod';
import { BillingStrategyProviderService } from '@kit/billing';
import { getLogger } from '@kit/shared/logger';
import { createYourProviderClient } from './your-provider-sdk';
export class YourProviderBillingStrategyService
implements BillingStrategyProviderService
{
private readonly namespace = 'billing.{provider}';
async createCheckoutSession(
params: z.infer<typeof CreateBillingCheckoutSchema>,
) {
const logger = await getLogger();
const client = await createYourProviderClient();
const ctx = {
name: this.namespace,
customerId: params.customerId,
accountId: params.accountId,
};
logger.info(ctx, 'Creating checkout session...');
try {
const response = await client.checkout.create({
customer: {
id: params.customerId,
email: params.customerEmail,
},
lineItems: params.plan.lineItems.map((item) => ({
priceId: item.id,
quantity: 1,
})),
successUrl: params.returnUrl,
metadata: {
accountId: params.accountId,
},
});
logger.info(ctx, 'Checkout session created successfully');
return {
checkoutToken: response.id,
};
} catch (error) {
logger.error({ ...ctx, error }, 'Failed to create checkout session');
throw new Error('Failed to create checkout session');
}
}
async cancelSubscription(
params: z.infer<typeof CancelSubscriptionParamsSchema>,
) {
const logger = await getLogger();
const client = await createYourProviderClient();
const ctx = {
name: this.namespace,
subscriptionId: params.subscriptionId,
};
logger.info(ctx, 'Cancelling subscription...');
try {
await client.subscriptions.cancel(params.subscriptionId, {
immediate: params.invoiceNow ?? true,
});
logger.info(ctx, 'Subscription cancelled successfully');
return { success: true };
} catch (error) {
logger.error({ ...ctx, error }, 'Failed to cancel subscription');
throw new Error('Failed to cancel subscription');
}
}
// Implement other required methods...
}

SDK Client Wrapper

Create a reusable SDK client:

// src/services/{provider}-sdk.ts
import 'server-only';
import { YourProviderServerEnvSchema } from '../schema/{provider}-server-env.schema';
export async function createYourProviderClient() {
// parse the environment variables
const config = YourProviderServerEnvSchema.parse({
apiKey: process.env.{PROVIDER}_API_KEY,
webhooksSecret: process.env.{PROVIDER}_WEBHOOK_SECRET,
});
return new YourProviderSDK({
apiKey: config.apiKey,
});
}

Webhook Handler Service

Implementation Pattern

import { BillingWebhookHandlerService, PlanTypeMap } from '@kit/billing';
import { getLogger } from '@kit/shared/logger';
import { createYourProviderClient } from './your-provider-sdk';
export class YourProviderWebhookHandlerService
implements BillingWebhookHandlerService
{
constructor(private readonly planTypesMap: PlanTypeMap) {}
private readonly provider = '{provider}' as const;
private readonly namespace = 'billing.{provider}';
async verifyWebhookSignature(request: Request) {
const body = await request.clone().text();
const signature = request.headers.get('{provider}-signature');
if (!signature) {
throw new Error('Missing {provider} signature');
}
const { webhooksSecret } = YourProviderServerEnvSchema.parse({
apiKey: process.env.{PROVIDER}_API_KEY,
webhooksSecret: process.env.{PROVIDER}_WEBHOOK_SECRET,
environment: process.env.{PROVIDER}_ENVIRONMENT || 'sandbox',
});
const client = await createYourProviderClient();
try {
const eventData = await client.webhooks.verify(body, signature, webhooksSecret);
if (!eventData) {
throw new Error('Invalid signature');
}
return eventData;
} catch (error) {
throw new Error(`Webhook signature verification failed: ${error}`);
}
}
async handleWebhookEvent(event: unknown, params) {
const logger = await getLogger();
switch (event.type) {
case 'checkout.session.completed': {
return this.handleCheckoutCompleted(event, params.onCheckoutSessionCompleted);
}
case 'customer.subscription.created':
case 'customer.subscription.updated': {
return this.handleSubscriptionUpdated(event, params.onSubscriptionUpdated);
}
case 'customer.subscription.deleted': {
return this.handleSubscriptionDeleted(event, params.onSubscriptionDeleted);
}
default: {
logger.info(
{
name: this.namespace,
eventType: event.type,
},
'Unhandled webhook event type',
);
if (params.onEvent) {
await params.onEvent(event);
}
}
}
}
private async handleCheckoutCompleted(event, onCheckoutSessionCompleted) {
// Extract subscription/order data from event
// Transform to standard format
// Call onCheckoutSessionCompleted with normalized data
}
// Implement other event handlers...
}

Client-Side Components

Checkout Component

Create a React component for the checkout flow:

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { YourProviderClientEnvSchema } from '../schema/{provider}-client-env.schema';
interface YourProviderCheckoutProps {
onClose?: () => void;
checkoutToken: string;
}
const config = YourProviderClientEnvSchema.parse({
publicKey: process.env.NEXT_PUBLIC_{PROVIDER}_PUBLIC_KEY,
environment: process.env.NEXT_PUBLIC_{PROVIDER}_ENVIRONMENT || 'sandbox',
});
export function YourProviderCheckout({
onClose,
checkoutToken,
}: YourProviderCheckoutProps) {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function initializeCheckout() {
try {
// Initialize provider's JavaScript SDK
const { YourProviderSDK } = await import('{provider}-js-sdk');
const sdk = new YourProviderSDK({
publicKey: config.publicKey,
environment: config.environment,
});
// Open checkout
await sdk.redirectToCheckout({
sessionId: checkoutToken,
successUrl: window.location.href,
cancelUrl: window.location.href,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Checkout failed';
setError(errorMessage);
onClose?.();
}
}
void initializeCheckout();
}, [checkoutToken, onClose]);
if (error) {
throw new Error(error);
}
return null; // Provider handles the UI
}

Registration and Integration

Register Billing Strategy

Add your provider to the billing strategy registry:

// packages/billing/gateway/src/server/services/billing-gateway/billing-gateway-registry.ts
// Register {Provider} billing strategy
billingStrategyRegistry.register('{provider}', async () => {
const { YourProviderBillingStrategyService } = await import('@kit/{provider}');
return new YourProviderBillingStrategyService();
});

Register Webhook Handler

Add your provider to the webhook handler factory:

// packages/billing/gateway/src/server/services/billing-event-handler/billing-event-handler-factory.service.ts
// Register {Provider} webhook handler
billingWebhookHandlerRegistry.register('{provider}', async () => {
const { YourProviderWebhookHandlerService } = await import('@kit/{provider}');
return new YourProviderWebhookHandlerService(planTypesMap);
});

Update Package Exports

Export your services from the main index file:

// packages/{provider}/src/index.ts
export { YourProviderBillingStrategyService } from './services/{provider}-billing-strategy.service';
export { YourProviderWebhookHandlerService } from './services/{provider}-webhook-handler.service';
export * from './components';
export * from './constants/{provider}-events';
export {
YourProviderClientEnvSchema,
type YourProviderClientEnv,
} from './schema/{provider}-client-env.schema';
export {
YourProviderServerEnvSchema,
type YourProviderServerEnv,
} from './schema/{provider}-server-env.schema';

Security Best Practices

Environment Variables

  1. Never expose secrets in client-side code
  2. Use different credentials for sandbox and production
  3. Validate all environment variables with Zod schemas
  4. Store secrets securely (e.g., in environment variables or secret managers)

Webhook Security

  1. Always verify webhook signatures
  2. Use HTTPS endpoints for webhooks
  3. Log security events for monitoring

Data Handling

  1. Validate all incoming data with Zod schemas
  2. Sanitize user inputs
  3. Never log sensitive information (API keys, customer data)
  4. Use structured logging with appropriate log levels

Error Handling

  1. Don't expose internal errors to users
  2. Log errors with sufficient context for debugging
  3. Implement proper error boundaries in React components
  4. Handle rate limiting and API errors gracefully

Example Implementation

For a complete reference implementation, see the Stripe integration at packages/billing/stripe/. Key files to study:

  • src/services/stripe-billing-strategy.service.ts - Complete billing strategy implementation
  • src/services/stripe-webhook-handler.service.ts - Webhook handling patterns
  • src/components/stripe-embedded-checkout.tsx - Client-side checkout component
  • src/schema/ - Environment configuration schemas

Also take a look at the Lemon Squeezy integration at packages/billing/lemon-squeezy/ or the Paddle integration at packages/plugins/paddle/ (in the plugins repository)

Conclusion

NB: different providers have different APIs, so the implementation will be different for each provider.

Following this guide, you should be able to create a robust billing integration that:

  • Implements all required interfaces correctly
  • Handles errors gracefully and securely
  • Provides a good user experience
  • Follows established patterns and best practices
  • Integrates seamlessly with the existing billing system

Remember to:

  1. Test thoroughly with the provider's sandbox environment
  2. Follow security best practices throughout development
  3. Document any provider-specific requirements or limitations
  4. Consider edge cases and error scenarios
  5. Validate your implementation against the existing test suite
On this page
  1. Architecture Overview
    1. Core Components
    2. Provider Structure
    3. Data Flow
  2. Creating a package
    1. Package Structure
      1. package.json Template
    2. Core Interface Implementation
      1. BillingStrategyProviderService
      2. BillingWebhookHandlerService
    3. Environment Configuration
      1. Server Environment Schema
      2. Client Environment Schema
    4. Billing Strategy Service
      1. Implementation Example
      2. SDK Client Wrapper
    5. Webhook Handler Service
      1. Implementation Pattern
    6. Client-Side Components
      1. Checkout Component
    7. Registration and Integration
      1. Register Billing Strategy
      2. Register Webhook Handler
      3. Update Package Exports
    8. Security Best Practices
      1. Environment Variables
      2. Webhook Security
      3. Data Handling
      4. Error Handling
    9. Example Implementation
      1. Conclusion