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 providerconst service = await createBillingGatewayService( process.env.NEXT_PUBLIC_BILLING_PROVIDER);// Or specify a provider explicitlyconst 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:
| Field | Type | Required | Description |
|---|---|---|---|
accountId | string | Yes | UUID of the account making the purchase |
plan | Plan | Yes | Plan object from your billing config |
returnUrl | string | Yes | URL to redirect after checkout |
customerEmail | string | No | Pre-fill customer email |
customerId | string | No | Existing customer ID (skips customer creation) |
enableDiscountField | boolean | No | Show coupon/discount input |
variantQuantities | array | No | Override 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.tsximport { 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 URLParameters:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID from billing provider |
returnUrl | string | Yes | URL 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:
| Field | Type | Required | Description |
|---|---|---|---|
subscriptionId | string | Yes | Subscription ID from provider |
invoiceNow | boolean | No | Invoice 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:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Customer ID (Stripe) or subscription item ID (LS) |
eventName | string | Stripe only | Meter event name |
usage.quantity | number | Yes | Usage amount |
usage.action | 'increment' | 'set' | No | How 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:
| Field | Type | Required | Description |
|---|---|---|---|
subscriptionId | string | Yes | Subscription ID |
subscriptionItemId | string | Yes | Line item ID within subscription |
quantity | number | Yes | New quantity (minimum 1) |
Automatic seat updates
For per-seat billing, Makerkit automatically updates seat counts when team members are added or removed. You typically don't need to call this directly.
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
Related Documentation
- Billing Overview - Architecture and concepts
- Webhooks - Handle billing events
- Metered Usage - Usage-based billing guide
- Per-Seat Billing - Team-based pricing