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:

SettingDescription
creditsPerDollarExchange rate (e.g., 100 credits = $1)
displayNameWhat to call credits (e.g., "Credits", "Tokens")
displaySymbolSymbol (e.g., "CR", "$")
displayDecimalsDecimal places for display
minPurchaseCents / maxPurchaseCentsPurchase limits
overdrawEnabledAllow balance to go negative
manualTopUpAvailableEnable manual credit purchases
autoTopUpAvailableEnable 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 });
});