Credits
Sell prepaid credits and refresh billing state after successful checkout.
AI skill for credits
/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 IDpriceId— Stripe product price IDname— Display name for the plandescription— Brief description of the planamount— Package pricecreditsAmount— Number of credits included in the packagecurrency— Currency, default usd
Examples
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=successassuccessUrl?billing=cancelascancelUrl
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