Billing API Reference for Next.js Supabase SaaS Kit

Complete API reference for Makerkit's billing service. Create checkouts, manage subscriptions, report usage, and handle billing operations programmatically.

The Billing Gateway Service provides a unified API for all billing operations, regardless of which payment provider you use (Stripe, Lemon Squeezy, or Paddle). This abstraction lets you switch providers without changing your application code.

Getting the Billing Service

import { createBillingGatewayService } from '@kit/billing-gateway';
// Get service for the configured provider
const service = await createBillingGatewayService(
process.env.NEXT_PUBLIC_BILLING_PROVIDER
);
// Or specify a provider explicitly
const stripeService = await createBillingGatewayService('stripe');

For most operations, get the provider from the user's subscription record:

import { createAccountsApi } from '@kit/accounts/api';
const accountsApi = createAccountsApi(supabaseClient);
const subscription = await accountsApi.getSubscription(accountId);
const provider = subscription?.billing_provider ?? 'stripe';
const service = await createBillingGatewayService(provider);

Create Checkout Session

Start a new subscription or one-off purchase.

const { checkoutToken } = await service.createCheckoutSession({
accountId: 'uuid-of-account',
plan: billingConfig.products[0].plans[0], // From billing.config.ts
returnUrl: 'https://yourapp.com/billing/return',
customerEmail: 'user@example.com', // Optional
customerId: 'cus_xxx', // Optional, if customer already exists
enableDiscountField: true, // Optional, show coupon input
variantQuantities: [ // Optional, for per-seat billing
{ variantId: 'price_xxx', quantity: 5 }
],
});

Parameters:

FieldTypeRequiredDescription
accountIdstringYesUUID of the account making the purchase
planPlanYesPlan object from your billing config
returnUrlstringYesURL to redirect after checkout
customerEmailstringNoPre-fill customer email
customerIdstringNoExisting customer ID (skips customer creation)
enableDiscountFieldbooleanNoShow coupon/discount input
variantQuantitiesarrayNoOverride quantities for line items

Returns:

{
checkoutToken: string // Token to open checkout UI
}

Example: Server Action

'use server';
import { createBillingGatewayService } from '@kit/billing-gateway';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import billingConfig from '~/config/billing.config';
export async function createCheckout(planId: string, accountId: string) {
const supabase = getSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('Not authenticated');
}
const plan = billingConfig.products
.flatMap(p => p.plans)
.find(p => p.id === planId);
if (!plan) {
throw new Error('Plan not found');
}
const service = await createBillingGatewayService(billingConfig.provider);
const { checkoutToken } = await service.createCheckoutSession({
accountId,
plan,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/billing/return`,
customerEmail: user.email,
});
return { checkoutToken };
}

Retrieve Checkout Session

Check the status of a checkout session after redirect.

const session = await service.retrieveCheckoutSession({
sessionId: 'cs_xxx', // From URL params after redirect
});

Returns:

{
checkoutToken: string | null,
status: 'complete' | 'expired' | 'open',
isSessionOpen: boolean,
customer: {
email: string | null
}
}

Example: Return page handler

// app/billing/return/page.tsx
import { createBillingGatewayService } from '@kit/billing-gateway';
export default async function BillingReturnPage({
searchParams,
}: {
searchParams: { session_id?: string }
}) {
if (!searchParams.session_id) {
return <div>Invalid session</div>;
}
const service = await createBillingGatewayService('stripe');
const session = await service.retrieveCheckoutSession({
sessionId: searchParams.session_id,
});
if (session.status === 'complete') {
return <div>Payment successful!</div>;
}
return <div>Payment pending or failed</div>;
}

Create Billing Portal Session

Open the customer portal for subscription management.

const { url } = await service.createBillingPortalSession({
customerId: 'cus_xxx', // From billing_customers table
returnUrl: 'https://yourapp.com/billing',
});
// Redirect user to the portal URL

Parameters:

FieldTypeRequiredDescription
customerIdstringYesCustomer ID from billing provider
returnUrlstringYesURL to redirect after portal session

Example: Server Action

'use server';
import { redirect } from 'next/navigation';
import { createBillingGatewayService } from '@kit/billing-gateway';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export async function openBillingPortal(accountId: string) {
const supabase = getSupabaseServerClient();
const api = createAccountsApi(supabase);
const customerId = await api.getCustomerId(accountId);
if (!customerId) {
throw new Error('No billing customer found');
}
const service = await createBillingGatewayService('stripe');
const { url } = await service.createBillingPortalSession({
customerId,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/billing`,
});
redirect(url);
}

Cancel Subscription

Cancel a subscription immediately or at period end.

const { success } = await service.cancelSubscription({
subscriptionId: 'sub_xxx',
invoiceNow: false, // Optional: charge immediately for usage
});

Parameters:

FieldTypeRequiredDescription
subscriptionIdstringYesSubscription ID from provider
invoiceNowbooleanNoInvoice outstanding usage immediately

Example: Cancel at period end

'use server';
import { createBillingGatewayService } from '@kit/billing-gateway';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export async function cancelSubscription(accountId: string) {
const supabase = getSupabaseServerClient();
const api = createAccountsApi(supabase);
const subscription = await api.getSubscription(accountId);
if (!subscription) {
throw new Error('No subscription found');
}
const service = await createBillingGatewayService(subscription.billing_provider);
await service.cancelSubscription({
subscriptionId: subscription.id,
});
return { success: true };
}

Report Usage (Metered Billing)

Report usage for metered billing subscriptions.

Stripe

Stripe uses customer ID and a meter event name:

await service.reportUsage({
id: 'cus_xxx', // Customer ID
eventName: 'api_requests', // Meter name in Stripe
usage: {
quantity: 100,
},
});

Lemon Squeezy

Lemon Squeezy uses subscription item ID:

await service.reportUsage({
id: 'sub_item_xxx', // Subscription item ID
usage: {
quantity: 100,
action: 'increment', // or 'set'
},
});

Parameters:

FieldTypeRequiredDescription
idstringYesCustomer ID (Stripe) or subscription item ID (LS)
eventNamestringStripe onlyMeter event name
usage.quantitynumberYesUsage amount
usage.action'increment' | 'set'NoHow to apply usage (LS only)

Example: Track API usage

import { createBillingGatewayService } from '@kit/billing-gateway';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export async function trackApiUsage(accountId: string, requestCount: number) {
const supabase = getSupabaseServerClient();
const api = createAccountsApi(supabase);
const subscription = await api.getSubscription(accountId);
if (!subscription || subscription.status !== 'active') {
return; // No active subscription
}
const service = await createBillingGatewayService(subscription.billing_provider);
const customerId = await api.getCustomerId(accountId);
if (subscription.billing_provider === 'stripe') {
await service.reportUsage({
id: customerId!,
eventName: 'api_requests',
usage: { quantity: requestCount },
});
} else {
// Lemon Squeezy: need subscription item ID
const { data: item } = await supabase
.from('subscription_items')
.select('id')
.eq('subscription_id', subscription.id)
.eq('type', 'metered')
.single();
if (item) {
await service.reportUsage({
id: item.id,
usage: { quantity: requestCount, action: 'increment' },
});
}
}
}

Query Usage

Retrieve usage data for a metered subscription.

Stripe

const usage = await service.queryUsage({
id: 'meter_xxx', // Stripe Meter ID
customerId: 'cus_xxx',
filter: {
startTime: Math.floor(Date.now() / 1000) - 86400 * 30, // 30 days ago
endTime: Math.floor(Date.now() / 1000),
},
});

Lemon Squeezy

const usage = await service.queryUsage({
id: 'sub_item_xxx', // Subscription item ID
customerId: 'cus_xxx',
filter: {
page: 1,
size: 100,
},
});

Returns:

{
value: number // Total usage in period
}

Update Subscription Item

Update the quantity of a subscription item (e.g., seat count).

const { success } = await service.updateSubscriptionItem({
subscriptionId: 'sub_xxx',
subscriptionItemId: 'si_xxx',
quantity: 10,
});

Parameters:

FieldTypeRequiredDescription
subscriptionIdstringYesSubscription ID
subscriptionItemIdstringYesLine item ID within subscription
quantitynumberYesNew quantity (minimum 1)

Get Subscription Details

Retrieve subscription details from the provider.

const subscription = await service.getSubscription('sub_xxx');

Returns: Provider-specific subscription object.

Get Plan Details

Retrieve plan/price details from the provider.

const plan = await service.getPlanById('price_xxx');

Returns: Provider-specific plan/price object.

Error Handling

All methods can throw errors. Wrap calls in try-catch:

try {
const { checkoutToken } = await service.createCheckoutSession({
// ...
});
} catch (error) {
if (error instanceof Error) {
console.error('Billing error:', error.message);
}
// Handle error appropriately
}

Common errors:

  • Invalid API keys
  • Invalid price/plan IDs
  • Customer not found
  • Subscription not found
  • Network/provider errors