Billing API Reference for Tanstack Start 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, routed to the configured payment provider. The kit ships with Stripe; because calls go through the gateway, adding another provider does not require changing your application code.

Getting the Billing Service

import { createBillingGatewayService } from '@kit/billing-gateway';
// Get service for the configured provider
const service = createBillingGatewayService(
process.env.VITE_BILLING_PROVIDER
);
// Or specify a provider explicitly
const stripeService = 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 = 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 Function

import { createServerFn } from '@tanstack/react-start';
import * as z from 'zod';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { createBillingGatewayService } from '@kit/billing-gateway';
import billingConfig from '#/config/billing.config.ts';
export const createCheckoutFunction = createServerFn({ method: 'POST' })
.middleware(authFunctionMiddleware)
.validator(z.object({ planId: z.string(), accountId: z.string() }))
.handler(async ({ data, context }) => {
const plan = billingConfig.products
.flatMap((p) => p.plans)
.find((p) => p.id === data.planId);
if (!plan) {
throw new Error('Plan not found');
}
const service = createBillingGatewayService(billingConfig.provider);
const { checkoutToken } = await service.createCheckoutSession({
accountId: data.accountId,
plan,
returnUrl: `${process.env.VITE_SITE_URL}/billing/return`,
customerEmail: context.user.email,
});
return { checkoutToken };
});

Call it from a client component with useServerFn and TanStack Query:

import { useServerFn } from '@tanstack/react-start';
import { useMutation } from '@tanstack/react-query';
import { createCheckoutFunction } from './checkout.functions';
function useCreateCheckout() {
const createCheckout = useServerFn(createCheckoutFunction);
return useMutation({
mutationFn: (data: { planId: string; accountId: string }) =>
createCheckout({ data }),
});
}

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 route

Fetch the session in a route loader (via a server function) and read the result in the component with Route.useLoaderData():

apps/web/src/routes/_authenticated/settings/billing/return.tsx

import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import * as z from 'zod';
import { createBillingGatewayService } from '@kit/billing-gateway';
const retrieveSession = createServerFn({ method: 'GET' })
.validator(z.object({ sessionId: z.string() }))
.handler(async ({ data }) => {
const service = createBillingGatewayService('stripe');
const session = await service.retrieveCheckoutSession({
sessionId: data.sessionId,
});
return { status: session.status };
});
export const Route = createFileRoute('/_authenticated/settings/billing/return')({
validateSearch: (search: Record<string, unknown>) => ({
session_id:
typeof search.session_id === 'string' ? search.session_id : undefined,
}),
loaderDeps: ({ search }) => ({ sessionId: search.session_id }),
loader: ({ deps }) => {
if (!deps.sessionId) {
return { status: null };
}
return retrieveSession({ data: { sessionId: deps.sessionId } });
},
component: BillingReturnPage,
});
function BillingReturnPage() {
const { status } = Route.useLoaderData();
if (!status) {
return <div>Invalid session</div>;
}
if (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 Function

import { createServerFn } from '@tanstack/react-start';
import * as z from 'zod';
import { createAccountsApi } from '@kit/accounts/api';
import { createBillingGatewayService } from '@kit/billing-gateway';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export const openBillingPortalFunction = createServerFn({ method: 'POST' })
.middleware(authFunctionMiddleware)
.validator(z.object({ accountId: z.string() }))
.handler(async ({ data }) => {
const supabase = getSupabaseServerClient();
const api = createAccountsApi(supabase);
const customerId = await api.getCustomerId(data.accountId);
if (!customerId) {
throw new Error('No billing customer found');
}
const service = createBillingGatewayService('stripe');
const { url } = await service.createBillingPortalSession({
customerId,
returnUrl: `${process.env.VITE_SITE_URL}/billing`,
});
// Return the external portal URL so the client can navigate to it with
// window.location.assign(url).
return { 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

import { createServerFn } from '@tanstack/react-start';
import * as z from 'zod';
import { createAccountsApi } from '@kit/accounts/api';
import { createBillingGatewayService } from '@kit/billing-gateway';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export const cancelSubscriptionFunction = createServerFn({ method: 'POST' })
.middleware(authFunctionMiddleware)
.validator(z.object({ accountId: z.string() }))
.handler(async ({ data }) => {
const supabase = getSupabaseServerClient();
const api = createAccountsApi(supabase);
const subscription = await api.getSubscription(data.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 uses the 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,
},
});

Parameters:

FieldTypeRequiredDescription
idstringYesStripe customer ID
eventNamestringYes (Stripe)Meter event name
usage.quantitynumberYesUsage amount
usage.action'increment' | 'set'NoAccepted by the schema; not used by the Stripe implementation

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 = createBillingGatewayService(subscription.billing_provider);
const customerId = await api.getCustomerId(accountId);
if (!customerId) {
return;
}
await service.reportUsage({
id: customerId,
eventName: 'api_requests',
usage: { quantity: requestCount },
});
}

Query Usage

Retrieve usage data for a metered subscription. Stripe requires a time filter (startTime and endTime):

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),
},
});

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