--- status: "published" label: "Billing API" title: "Billing API Reference for Next.js Supabase SaaS Kit" order: 10 description: "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 ```tsx import { createBillingGatewayService } from '@kit/billing-gateway'; // Get service for the configured provider const service = createBillingGatewayService( process.env.NEXT_PUBLIC_BILLING_PROVIDER ); // Or specify a provider explicitly const stripeService = createBillingGatewayService('stripe'); ``` For most operations, get the provider from the user's subscription record: ```tsx import { createAccountsApi } from '@kit/accounts/api'; const accountsApi = createAccountsApi(supabaseClient); const subscription = await accountsApi.getSubscription(accountId); const provider = subscription?.billing_provider ?? 'stripe'; const service = createBillingGatewayService(provider); ``` ## Create Checkout Session Start a new subscription or one-off purchase. ```tsx 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:** ```tsx { checkoutToken: string // Token to open checkout UI } ``` **Example: Server Action** ```tsx '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 = 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. ```tsx const session = await service.retrieveCheckoutSession({ sessionId: 'cs_xxx', // From URL params after redirect }); ``` **Returns:** ```tsx { checkoutToken: string | null, status: 'complete' | 'expired' | 'open', isSessionOpen: boolean, customer: { email: string | null } } ``` **Example: Return page handler** ```tsx // app/[locale]/home/[account]/billing/return/page.tsx import { createBillingGatewayService } from '@kit/billing-gateway'; export default async function BillingReturnPage({ searchParams, }: { searchParams: Promise<{ session_id?: string }> }) { const { session_id } = await searchParams; if (!session_id) { return
Invalid session
; } const service = createBillingGatewayService('stripe'); const session = await service.retrieveCheckoutSession({ sessionId: session_id, }); if (session.status === 'complete') { return
Payment successful!
; } return
Payment pending or failed
; } ``` ## Create Billing Portal Session Open the customer portal for subscription management. ```tsx const { url } = await service.createBillingPortalSession({ customerId: 'cus_xxx', // From billing_customers table returnUrl: 'https://yourapp.com/billing', }); // Redirect user to the portal URL ``` **Parameters:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `customerId` | `string` | Yes | Customer ID from billing provider | | `returnUrl` | `string` | Yes | URL to redirect after portal session | **Example: Server Action** ```tsx '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 = 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. ```tsx 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** ```tsx '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 = 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: ```tsx 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: ```tsx 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** ```tsx 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 = 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 ```tsx 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 ```tsx const usage = await service.queryUsage({ id: 'sub_item_xxx', // Subscription item ID customerId: 'cus_xxx', filter: { page: 1, size: 100, }, }); ``` **Returns:** ```tsx { value: number // Total usage in period } ``` ## Update Subscription Item Update the quantity of a subscription item (e.g., seat count). ```tsx 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) | {% alert type="default" title="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. {% /alert %} ## Get Subscription Details Retrieve subscription details from the provider. ```tsx const subscription = await service.getSubscription('sub_xxx'); ``` **Returns:** Provider-specific subscription object. ## Get Plan Details Retrieve plan/price details from the provider. ```tsx 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: ```tsx 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](/docs/next-supabase-turbo/billing/overview) - Architecture and concepts - [Webhooks](/docs/next-supabase-turbo/billing/billing-webhooks) - Handle billing events - [Metered Usage](/docs/next-supabase-turbo/billing/metered-usage) - Usage-based billing guide - [Per-Seat Billing](/docs/next-supabase-turbo/billing/per-seat-billing) - Team-based pricing