Provider Development
Learn how to create custom payment providers and capabilities
Provider Development
This guide shows you how to create custom payment providers and add new capabilities to existing providers in Better Billing. Providers are the core way to add payment processing and billing functionality.
Provider Architecture
Providers are organized by capabilities rather than provider names. This allows multiple plugins to extend the same provider with different capabilities.
// Provider structure
{
providerId: "stripe", // Unique provider identifier
capability: "subscription", // What this provider can do
methods: { // Available methods for this capability
createSubscription: async (params) => { /* ... */ },
cancelSubscription: async (params) => { /* ... */ },
// ... more methods
}
}Built-in Capabilities
Better Billing defines several standard capabilities:
Subscription Capability
Methods for managing recurring subscriptions:
interface SubscriptionCapability {
createSubscription(params: CreateSubscriptionParams): Promise<Subscription>;
cancelSubscription(params: CancelSubscriptionParams): Promise<void>;
updateSubscription(params: UpdateSubscriptionParams): Promise<Subscription>;
getSubscription(params: GetSubscriptionParams): Promise<Subscription>;
}Checkout Session Capability
Methods for creating payment sessions:
interface CheckoutSessionCapability {
createCheckoutSession(params: CreateCheckoutParams): Promise<CheckoutSession>;
getCheckoutSession(params: GetCheckoutParams): Promise<CheckoutSession>;
}Creating a Custom Provider
Here's how to create a custom payment provider:
import { createPlugin } from "better-billing";
import { corePlugin } from "better-billing/plugins/core";
const customPaymentPlugin = createPlugin(
(deps) => {
return {
providers: [
{
providerId: "custom-pay",
capability: "subscription" as const,
methods: {
createSubscription: async (params) => {
// Call your payment processor API
const response = await fetch("https://api.custompay.com/subscriptions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CUSTOM_PAY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customerId: params.billableId,
planId: params.planName,
trialDays: params.trialDays,
}),
});
const subscription = await response.json();
// Save to database using Better Billing's database adapter
await deps.db.create("subscriptions", {
id: subscription.id,
billableId: params.billableId,
planName: params.planName,
status: "active",
providerId: "custom-pay",
providerSubscriptionId: subscription.id,
currentPeriodStart: new Date(subscription.currentPeriodStart),
currentPeriodEnd: new Date(subscription.currentPeriodEnd),
createdAt: new Date(),
updatedAt: new Date(),
});
return subscription;
},
cancelSubscription: async (params) => {
// Call API to cancel subscription
await fetch(`https://api.custompay.com/subscriptions/${params.subscriptionId}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${process.env.CUSTOM_PAY_API_KEY}`,
},
});
// Update database
await deps.db.update("subscriptions", params.subscriptionId, {
status: "canceled",
updatedAt: new Date(),
});
},
updateSubscription: async (params) => {
// Implementation for updating subscription
const response = await fetch(`https://api.custompay.com/subscriptions/${params.subscriptionId}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.CUSTOM_PAY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
planId: params.planName,
}),
});
const subscription = await response.json();
// Update database
await deps.db.update("subscriptions", params.subscriptionId, {
planName: params.planName,
updatedAt: new Date(),
});
return subscription;
},
getSubscription: async (params) => {
// Fetch from database or API
const subscription = await deps.db.findFirst("subscriptions", {
id: params.subscriptionId,
});
return subscription;
},
},
},
],
};
},
{
dependsOn: [corePlugin] as const,
}
);Adding Capabilities to Existing Providers
You can extend existing providers with new capabilities:
const stripeExtensionPlugin = createPlugin(
(deps) => {
return {
providers: [
{
providerId: "stripe", // Same provider ID
capability: "usage-tracking" as const, // New capability
methods: {
trackUsage: async (params) => {
// Add usage tracking to Stripe
const stripe = deps.stripe; // Access Stripe from dependencies
await stripe.subscriptionItems.createUsageRecord(
params.subscriptionItemId,
{
quantity: params.quantity,
timestamp: params.timestamp,
}
);
},
getUsage: async (params) => {
// Get usage data from Stripe
const usage = await stripe.subscriptionItems.listUsageRecordSummaries(
params.subscriptionItemId
);
return usage.data;
},
},
},
],
};
},
{
dependsOn: [corePlugin, stripePlugin] as const,
}
);Provider Method Patterns
Standard Parameters
Most provider methods follow common parameter patterns:
// Billable entity identification
interface BillableParams {
billableId: string; // ID of the entity being billed
billableType: string; // Type of entity ("user", "organization", etc.)
}
// Subscription parameters
interface SubscriptionParams extends BillableParams {
planName: string; // Must match configured plan names
trialDays?: number; // Optional trial period
metadata?: Record<string, string>; // Additional data
}Error Handling
Always handle errors appropriately in provider methods:
methods: {
createSubscription: async (params) => {
try {
// API call
const response = await paymentAPI.createSubscription(params);
if (!response.ok) {
throw new Error(`Payment API error: ${response.status}`);
}
// Database operation
await deps.db.create("subscriptions", subscriptionData);
return response.data;
} catch (error) {
// Log error for debugging
console.error("Failed to create subscription:", error);
// Re-throw with user-friendly message
throw new Error("Unable to create subscription. Please try again.");
}
},
}Async Operations
All provider methods should be async and handle database transactions properly:
methods: {
createSubscription: async (params) => {
// Use database transactions for consistency
return await deps.db.transaction(async (tx) => {
// Create customer if needed
const customer = await tx.findOrCreate("customers", {
externalId: params.billableId,
type: params.billableType,
});
// Create subscription
const subscription = await tx.create("subscriptions", {
billableId: customer.id,
planName: params.planName,
// ... other fields
});
return subscription;
});
},
}Type Safety
Better Billing provides full TypeScript support for provider development:
import type {
CreateSubscriptionParams,
Subscription,
BillingContext
} from "better-billing";
const myPlugin = createPlugin(
(deps: BillingContext) => {
return {
providers: [
{
providerId: "my-provider",
capability: "subscription" as const,
methods: {
// Fully typed parameters and return values
createSubscription: async (params: CreateSubscriptionParams): Promise<Subscription> => {
// Implementation
},
},
},
],
};
},
{
dependsOn: [corePlugin] as const,
}
);Testing Providers
Create comprehensive tests for your provider methods:
import { describe, it, expect, vi } from "vitest";
import { createTestBilling } from "better-billing/testing";
describe("Custom Payment Provider", () => {
it("should create subscription", async () => {
const billing = createTestBilling({
plugins: [corePlugin({}), customPaymentPlugin],
});
const subscription = await billing.providers["custom-pay"].createSubscription({
billableId: "user_123",
billableType: "user",
planName: "Pro",
});
expect(subscription.planName).toBe("Pro");
expect(subscription.status).toBe("active");
});
it("should handle API errors gracefully", async () => {
// Mock API failure
vi.mocked(fetch).mockRejectedValueOnce(new Error("API Error"));
const billing = createTestBilling({
plugins: [corePlugin({}), customPaymentPlugin],
});
await expect(
billing.providers["custom-pay"].createSubscription({
billableId: "user_123",
billableType: "user",
planName: "Pro",
})
).rejects.toThrow("Unable to create subscription");
});
});Best Practices
- Follow Standard Interfaces: Use established capability interfaces when possible
- Handle Errors Gracefully: Provide meaningful error messages to users
- Use Database Transactions: Ensure data consistency in multi-step operations
- Type Everything: Leverage TypeScript for better development experience
- Test Thoroughly: Write tests for success cases, error cases, and edge cases
- Document Methods: Add JSDoc comments for complex provider methods
- Validate Input: Always validate parameters before making API calls
Common Patterns
Webhook Handler Integration
Combine providers with webhook endpoints:
const webhookProviderPlugin = createPlugin(
(deps) => {
return {
providers: [
{
providerId: "webhook-provider",
capability: "subscription",
methods: {
// ... subscription methods
},
},
],
endpoints: {
webhook: createEndpoint("/webhook-provider/webhook", {
method: "POST",
path: "/webhook-provider/webhook",
}, async (request) => {
const payload = await request.json();
// Process webhook and update database
await deps.db.update("subscriptions", payload.subscriptionId, {
status: payload.status,
});
return { received: true };
}),
},
};
},
{
dependsOn: [corePlugin] as const,
}
);Configuration-Driven Providers
Accept configuration options:
const configurablePlugin = (config: { apiKey: string; baseUrl: string }) =>
createPlugin(
(deps) => {
return {
providers: [
{
providerId: "configurable",
capability: "subscription",
methods: {
createSubscription: async (params) => {
const response = await fetch(`${config.baseUrl}/subscriptions`, {
headers: { "Authorization": `Bearer ${config.apiKey}` },
// ... rest of implementation
});
},
},
},
],
};
},
{
dependsOn: [corePlugin] as const,
}
);
// Usage
const billing = betterBilling({
plugins: [
corePlugin({}),
configurablePlugin({
apiKey: process.env.API_KEY!,
baseUrl: "https://api.example.com",
}),
],
});Next Steps
- Schema Development - Define custom database schemas
- API Endpoints - Create webhook handlers
- Plugin Examples - See complete plugin implementations