# 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.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/billing/billing-api*

---

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.VITE_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 Function**

```tsx
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:

```tsx
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.

```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 route**

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

```tsx {% title="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.

```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 Function**

```tsx
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.

```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
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

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/tanstack-supabase/billing/overview) - Architecture and concepts
- [Webhooks](/docs/tanstack-supabase/billing/billing-webhooks) - Handle billing events
- [Metered Usage](/docs/tanstack-supabase/billing/metered-usage) - Usage-based billing guide
- [Per-Seat Billing](/docs/tanstack-supabase/billing/per-seat-billing) - Team-based pricing
