Documentation
Payments

Credits

Sell prepaid credits and refresh billing state after successful checkout.

AI skill for credits

Prompt: Type /payments in your Copilot / Cursor or other chat to use skill with the provided context.
/payments Update credits configurations or payment callbacks.

Use cases

How credits work

GoLiveKit uses a dual-bucket system to manage user credits:

  • Purchased Credits: Added when a user buys a credit package via Stripe checkout.
  • Bonus Credits: Added manually by admins (e.g., promotional rewards, sign-up bonuses, or compensation).
  • Available Credits: The total balance across both buckets (Purchased + Bonus).

Bonus-first deduction strategy

When a user spends credits, the system always deducts from the Bonus bucket first. Once the bonus credits are fully depleted, any remaining cost is deducted from the Purchased bucket. This ensures promotional credits are consumed before the credits a user actually paid for.

Configure plans

Enable credits

Open src/config/app.ts and set billing.credits.enabled: true.

Set price per credit

Update pricePerCredit: 0.01

Allow top up credits manually (optional)

Update enterManually: true if you want to enable manual credit top-ups.

Bonus credits for registration (optional)

Update initialBonusCredits if you want to grant bonus credits upon user registration.

Add credit packages

Under credits.packages, add your credit packages with their respective name, description, creditsAmount, priceId, and price.

Credits properties

  • id — Stripe product ID
  • priceId — Stripe product price ID
  • name — Display name for the plan
  • description — Brief description of the plan
  • amount — Package price
  • creditsAmount — Number of credits included in the package
  • currency — Currency, default usd

Examples

src/config/app.ts
credits: {
  enabled: true,
  enterManually: true,
  pricePerCredit: 0.01,
  packages: [
    {
      id: 'starter',
      priceId: 'starter',
      name: 'Starter Pack',
      description: '1000 credits for $10',
      amount: 10,
      currency: 'usd',
      creditsAmount: 1000,
    },
    {
      id: 'pro',
      priceId: 'pro',
      name: 'Pro Pack',
      description: '5000 credits for $40',
      amount: 40,
      currency: 'usd',
      creditsAmount: 5000,
    },
  ],
},

Add credits manually (admin or system)

Use addCreditsDirectly when you need a direct credit top-up without Stripe checkout:

import { addCreditsDirectly } from '@/features/common/payments/service/payment.service';

await addCreditsDirectly({
  userId,
  credits: 250,
  creditsType: 'bonus',
  reason: 'Welcome bonus',
});

Deduct credits with bonus-first strategy

Use deductCreditsBonusFirst in credit-consuming endpoints:

import {
  deductCreditsBonusFirst,
  InsufficientCreditsError,
} from '@/features/common/payments/service/credits-balance.service';

try {
  const result = await deductCreditsBonusFirst({
    userId,
    credits: 40,
    reason: 'Image generation',
  });

  console.log(result.creditsBalance);
} catch (error) {
  if (error instanceof InsufficientCreditsError) {
    // Return 402/400 depending on your API policy
  }
  throw error;
}

API endpoint example:

Example of wiring into an endpoint/service method:

import {
  deductCreditsBonusFirst,
  InsufficientCreditsError,
} from '@/features/common/payments/service/credits-balance.service';

export async function runGeneration(userId: string, prompt: string) {
  try {
    await deductCreditsBonusFirst({
      userId,
      credits: 10,
      reason: 'Run generation',
    });
  } catch (error) {
    if (error instanceof InsufficientCreditsError) {
      throw new Error('Not enough credits');
    }
    throw error;
  }

  // Continue paid operation after successful deduction
  return executeGeneration(prompt);
}

Show credits usage or Top-up balance modal

Use useCredits + extracted modal helpers in any client component to:

  • show available / purchased / bonus / used balances
  • start top-up from package buttons
  • open a top-up modal from any page
'use client';

import { CreditsTopUpModal } from '@/features/common/payments/components/credits-top-up-modal';
import { useCredits } from '@/features/common/payments/hooks/use-credits';
import { useCreditsTopUpModal } from '@/features/common/payments/hooks/use-credits-top-up-modal';
import { Button } from '@/shared/components/ui/button';

export function CreditsQuickAccess() {
  const modal = useCreditsTopUpModal();
  const {
    creditsBalance,
    creditsPurchased,
    creditsBonus,
    creditsUsed,
  } = useCredits();

  return (
    <div className='space-y-3'>
      <p>Available: {creditsBalance ?? 0}</p>
      <p>Purchased: {creditsPurchased ?? 0}</p>
      <p>Bonus: {creditsBonus ?? 0}</p>
      <p>Used: {creditsUsed ?? 0}</p>

      <Button onClick={modal.openTopUpModal}>Top up credits</Button>

      <CreditsTopUpModal
        open={modal.isOpen}
        onOpenChange={modal.setIsOpen}
      />
    </div>
  );
}

You can also pass a trigger directly and avoid manual button wiring:

<CreditsTopUpModal
  open={modal.isOpen}
  onOpenChange={modal.setIsOpen}
  trigger={<Button>Top up credits</Button>}
/>

Under the hood, the modal uses useCredits().topUp(...). By default, topUp(packageId) uses the current page URL and appends:

  • ?billing=success as successUrl
  • ?billing=cancel as cancelUrl

If needed, pass custom URLs:

await topUp('credits_starter', {
  successUrl: `${window.location.origin}/dashboard/billings/credits?billing=success`,
  cancelUrl: `${window.location.origin}/dashboard/billings/credits?billing=cancel`,
});

To set custom URLs globally for this modal, extend CreditsTopUpModal props and pass them to topUp inside the component.

Payment callbacks

You can add your callbacks to Stripe payment events to implement extra logic (e.g., 3rd party API calls, logging, metadata updates). Review the callback service in src/features/common/payments/service/payment-callback.service.ts.

Credits Top-Up Success

Called after a credits top-up purchase is completed. Add custom logic here: webhooks, analytics events, 3rd-party integrations, etc.

export async function onCreditsTopUp(
  payload: CreditsTopUpPayload,
): Promise<void> {
  // Available fields in payload:
  // userId email, name, creditsAdded, amountText
  console.log('[credits] top-up', payload);
}

Emails

The system automatically sends emails based on top-up purchase lifecycle events.

  • Credits Top-Up: src/shared/emails/credits-top-up.tsx

FAQ

On this page