Credits & Balance System
The credits system lets you implement usage-based billing. Your server charges and deposits credits; users can purchase credits and set up auto top-up from the client.
Setup
Enable credits in the dashboard and configure:
| Setting | Description |
|---|---|
creditsPerDollar | Exchange rate (e.g., 100 credits = $1) |
displayName | What to call credits (e.g., "Credits", "Tokens") |
displaySymbol | Symbol (e.g., "CR", "$") |
displayDecimals | Decimal places for display |
minPurchaseCents / maxPurchaseCents | Purchase limits |
overdrawEnabled | Allow balance to go negative |
manualTopUpAvailable | Enable manual credit purchases |
autoTopUpAvailable | Enable auto top-up |
Server-Side Operations
Use the server SDK for charging and depositing credits. These operations are authenticated via API key.
Charging Credits
Deduct credits for usage:
import { createServerClient } from '@reauth-dev/sdk/server';
const reauth = createServerClient({
domain: process.env.REAUTH_DOMAIN!,
apiKey: process.env.REAUTH_API_KEY!,
});
// Charge 10 credits for an API call
const { newBalance } = await reauth.charge(userId, {
amount: 10,
requestUuid: `api-call-${requestId}`, // Idempotency key
note: 'API call: generate-report', // Optional
});
Error handling:
try {
await reauth.charge(userId, { amount: 10, requestUuid: opId });
} catch (err) {
if (err instanceof Error && err.message === 'Insufficient balance') {
// 402 — user doesn't have enough credits
return { error: 'Not enough credits', status: 402 };
}
throw err;
}
Depositing Credits
Add credits (bonuses, refunds, plan allocations):
await reauth.deposit(userId, {
amount: 100,
requestUuid: `welcome-${userId}`,
note: 'Welcome bonus',
});
Checking Balance
const { balance } = await reauth.getBalance(userId);
Transaction History
const { transactions } = await reauth.getTransactions(userId, {
limit: 50,
offset: 0,
});
// Each transaction: { id, amountDelta, reason, balanceAfter, createdAt }
// amountDelta is positive for deposits, negative for charges
Idempotency
All charge and deposit operations accept a requestUuid for idempotency. If the same requestUuid is sent twice, the second call returns the original result without applying the operation again.
// These patterns prevent double-charging:
// Tied to the operation being performed
const requestUuid = `report-${reportId}`;
// Tied to the user action
const requestUuid = `todo-create-${todoId}`;
// Random UUID for one-off operations
import { v4 as uuid } from 'uuid';
const requestUuid = uuid();
Client-Side Operations
Users interact with credits through the browser SDK.
Reading Balance
const { balance } = await reauth.getBalance();
const { transactions } = await reauth.getTransactions({ limit: 20 });
Credits Configuration
Check what's available:
const config = await reauth.getCreditsConfig();
// {
// creditsEnabled: boolean,
// creditsPerDollar: number,
// displayName: string, // e.g., "Credits"
// displaySymbol: string | null, // e.g., "CR"
// displaySymbolPosition: "left" | "right",
// displayDecimals: number,
// minPurchaseCents: number,
// maxPurchaseCents: number,
// manualTopUpAvailable: boolean,
// autoTopUpAvailable: boolean,
// overdrawEnabled: boolean,
// }
Purchasing Credits
Users buy credits with a stored payment method:
const result = await reauth.purchaseCredits({
amountCents: 1000, // $10.00
paymentMethodId: 'pm-uuid', // From getPaymentMethods()
idempotencyKey: 'purchase-123',
});
// { creditsPurchased: 1000, newBalance: 1500, paymentIntentId: "pi_..." }
Payment Methods
Users can store cards for credit purchases and auto top-up:
// List stored payment methods
const methods = await reauth.getPaymentMethods();
// [{ id, provider, methodType, cardBrand, cardLast4, cardExpMonth, cardExpYear, priority }]
// Add a new payment method using Stripe.js
const { clientSecret } = await reauth.createSetupIntent();
// Use clientSecret with Stripe.js:
// const stripe = Stripe('pk_...');
// await stripe.confirmCardSetup(clientSecret, { payment_method: { card: cardElement } });
// Delete a payment method
await reauth.deletePaymentMethod('pm-uuid');
// Reorder priority (first in list = highest priority)
await reauth.reorderPaymentMethods(['pm-primary', 'pm-backup']);
Auto Top-Up
Automatically purchase credits when balance drops below a threshold:
// Check current status
const status = await reauth.getAutoTopUpStatus();
// {
// enabled: boolean,
// thresholdCents: number,
// purchaseAmountCents: number,
// status: string, // "active", "paused", etc.
// lastFailureReason?: string,
// retriesRemaining?: number,
// nextRetryAt?: string,
// }
// Enable auto top-up
await reauth.updateAutoTopUp({
enabled: true,
thresholdCents: 500, // Top up when below $5
purchaseAmountCents: 2000, // Purchase $20 worth
});
// Disable
await reauth.updateAutoTopUp({
enabled: false,
thresholdCents: 0,
purchaseAmountCents: 0,
});
Full Example: Charging for API Usage
From the demo app — charging credits to create a todo:
const TODO_COST = 10;
app.post('/api/todos', authMiddleware, async (req, res) => {
const todoId = uuid();
// Charge credits (todoId as idempotency key prevents double-charge)
try {
await reauth.charge(req.user!.id, {
amount: TODO_COST,
requestUuid: todoId,
note: `Create todo: ${req.body.text.slice(0, 50)}`,
});
} catch (err) {
if (err instanceof Error && err.message === 'Insufficient balance') {
return res.status(402).json({ error: 'Not enough credits', required: TODO_COST });
}
return res.status(500).json({ error: 'Failed to charge credits' });
}
// Create the todo...
// If persistence fails, refund:
try {
await saveTodo(req.user!.id, { id: todoId, text: req.body.text });
} catch {
await reauth.deposit(req.user!.id, {
amount: TODO_COST,
requestUuid: `refund-${todoId}`,
note: 'Refund: failed to persist todo',
});
return res.status(500).json({ error: 'Failed to create todo' });
}
res.status(201).json({ id: todoId });
});