Skip to main content
Credits are first-class in UnitPay. These hooks read the customer’s credit wallets (one per credit currency), refill them by amount or by fixed-SKU package, automate refills at a threshold, and paginate the append-only ledger and top-up history. Every hook returns isLoading: boolean and error: Error | null (see Introduction); the sections below list each hook’s specific returns.

Two denominations

Every wallet has a denomination that decides how to render its balance and which top-up shape it accepts:
  • 'unit' — raw counts like “AI Credits” or API calls. Render 5,000 credits; auto-topup uses mode: 'package'.
  • A fiat code ('usd', 'eur', …) — a prepaid money balance with minorUnitScale decimal places. Render $50.00; auto-topup uses mode: 'amount'.
Branch on denomination; never mix the two formatters. A wallet balance is the credit wallet balance — unrelated to metered entitlement remaining (use entitlements for that).

useCreditAccounts

Returns every credit account (wallet) the customer holds — one per credit currency — each already carrying its balance, currency metadata, active grants, and auto-topup config. No second round-trip is needed for the typical wallet UI. Parameters
customerId
string
Optional. Defaults to the customer set on <UnitPayProvider>. Pass an explicit id only to read a different customer’s wallets.
Returns
  • accountsCreditAccount[] — all wallets for the customer (one per currency), or [] while loading or when the customer has no wallet yet. Render as a list of wallet cards.
import { useCreditAccounts } from '@unitpay/react';

export default function Wallets() {
  const { accounts, isLoading, error } = useCreditAccounts();

  if (isLoading) return <p>Loading wallets…</p>;
  if (error) return <p>Failed to load wallets.</p>;
  if (accounts.length === 0) return <p>No credit wallets yet.</p>;

  return (
    <ul>
      {accounts.map((acct) => {
        const denom = acct.currency?.denomination ?? 'unit';
        const label =
          denom === 'unit'
            ? `${acct.balance.toLocaleString()} credits`
            : `${(acct.balance / 10 ** (acct.currency?.minorUnitScale ?? 2)).toFixed(
                acct.currency?.minorUnitScale ?? 2,
              )} ${denom.toUpperCase()}`;
        return (
          <li key={acct.id}>
            {acct.currency?.name ?? acct.creditCurrencyId}{label} ({acct.status})
          </li>
        );
      })}
    </ul>
  );
}

useCreditCurrencies

Returns the org-wide list of active credit currencies the merchant has configured. Use it to build a “Wallets you can fund” picker when the customer hasn’t engaged any wallet yet (i.e. useCreditAccounts returns []). Admin-managed, so this list is read-only and cached aggressively. Parameters — takes no arguments; the list is org-scoped, resolved from the <UnitPayProvider> context. Returns
  • currenciesCreditCurrency[] — every active credit currency configured on the org, or [] while loading.
import { useCreditAccounts, useCreditCurrencies } from '@unitpay/react';

export default function FundableWallets() {
  const { accounts } = useCreditAccounts();
  const { currencies, isLoading } = useCreditCurrencies();

  if (isLoading) return <p>Loading…</p>;

  const available = currencies.filter(
    (c) => !accounts.some((a) => a.creditCurrencyId === c.id),
  );

  return (
    <ul>
      {available.map((c) => (
        <li key={c.id}>
          {c.name}{c.denomination === 'unit' ? 'credits' : c.denomination.toUpperCase()}
        </li>
      ))}
    </ul>
  );
}
The first useAutoTopUp set() or first useTopUp topUp() call on an unengaged currency auto-creates the credit_account server-side.

useTopUp

Refills the customer’s credit wallet by a free-form amount. The server dispatches on customer state — PLG with a payment method charges inline, SLG appends the top-up to the next invoice — and returns a SettleOutcome you wire to your UX with on* callbacks. ParametersuseTopUp(options?) accepts an optional HandleSettlementOptions object of settlement callbacks (onChargedInline, onInvoiceAdded, onRequiresForm, onNoAction, …). Each server outcome maps to exactly one; unhandled ones are silent no-ops. The returned topUp(input) alias takes:
creditCurrencyId
string
required
The wallet currency to refill. Auto-creates the credit_account server-side if the customer hasn’t engaged this currency yet.
amountCents
number
required
Amount to add, in minor units of the currency’s denomination (unit credits or fiat).
idempotencyKey
string
Override the auto-generated Idempotency-Key. Auto-generated by default.
forceForm
boolean
Force the inline <PaymentForm> even when a payment method is on file.
PLG customers with no payment method throw TOPUP_REQUIRES_PM (402) — route the customer to payment-method setup first.
Returns — a React Query mutation, plus a topUp alias for mutateAsync:
  • topUp(input)(input: UseTopUpInput) => Promise<SettleOutcome> — performs the top-up; the options callbacks fire automatically on success.
  • isPending / data — standard mutation state (isPending while in flight, data is the last SettleOutcome).
import { useTopUp } from '@unitpay/react';

export default function TopUpButton({ creditCurrencyId }: { creditCurrencyId: string }) {
  const { topUp, isPending } = useTopUp({
    onChargedInline: () => alert('Wallet topped up!'),
    onInvoiceAdded: ({ lineAmount }) =>
      alert(`Added ${lineAmount} to your next invoice.`),
    onRequiresForm: ({ client }) => {
      // mount <PaymentForm clientSecret={client.token} … />
    },
  });

  return (
    <button
      onClick={() => topUp({ creditCurrencyId, amountCents: 1000 })}
      disabled={isPending}
    >
      {isPending ? 'Processing…' : 'Add 1,000 credits'}
    </button>
  );
}

useBuyCreditPackage

Purchases a fixed-SKU credit package. Like useTopUp, the server dispatches on customer state — PLG with a payment method charges inline, SLG appends to the next invoice — and returns a SettleOutcome you wire to your UX with on* callbacks. A package is a pre-configured SKU (its credit amount, price, and target wallet are fixed merchant-side), so you only pass the package id. ParametersuseBuyCreditPackage(options?) accepts an optional HandleSettlementOptions object of settlement callbacks (same set as useTopUp). The returned buyCreditPackage(input) alias takes:
creditPackageId
string
required
The credit package SKU to purchase.
idempotencyKey
string
Override the auto-generated Idempotency-Key. Auto-generated by default.
forceForm
boolean
Force the inline <PaymentForm> even when a payment method is on file.
PLG customers with no payment method throw — route the customer to payment-method setup first.
Returns — a React Query mutation, plus a buyCreditPackage alias for mutateAsync:
  • buyCreditPackage(input)(input: UseBuyCreditPackageInput) => Promise<SettleOutcome> — performs the purchase; the options callbacks fire automatically on success.
  • isPending / data — standard mutation state.
import { useBuyCreditPackage } from '@unitpay/react';

export default function BuyPackageButton({ creditPackageId }: { creditPackageId: string }) {
  const { buyCreditPackage, isPending } = useBuyCreditPackage({
    onChargedInline: () => alert('Package purchased!'),
    onInvoiceAdded: ({ lineAmount }) =>
      alert(`Added ${lineAmount} to your next invoice.`),
    onRequiresForm: ({ client }) => {
      // mount <PaymentForm clientSecret={client.token} … />
    },
  });

  return (
    <button onClick={() => buyCreditPackage({ creditPackageId })} disabled={isPending}>
      {isPending ? 'Processing…' : 'Buy credit pack'}
    </button>
  );
}

useAutoTopUp

Manages the auto-topup rule for one credit currency. Reads piggyback on useCreditAccounts (no extra fetch — autoTopup and autoTopupRuntime are already inline on each wallet row); set() and disable() upsert the rule and re-fetch the wallet so the card reflects the new state immediately. The wallet’s denomination decides the rule shape: 'unit' (SKU credits) → set({ mode: 'package', … }); a fiat code → set({ mode: 'amount', … }). The server validates mode against the target currency’s denomination. All monetary fields (threshold, topupAmount, monthlyChargeLimit) are minor units of that denomination. Parameters
creditCurrencyId
string
required
The wallet currency whose auto-topup rule to manage.
Returns
  • ruleAutoTopupConfig | null — current config (enabled, threshold, packageId, amount, monthlyChargeLimit), or null when no credit_account exists for this currency yet.
  • runtimeAutoTopupRuntime | null — derived counters (lastFiredAt, lastFailedAt, consecutiveFailures, totalFiresCount, isPending, monthToDateCharge), or null with no account.
  • effectiveFundingMethod'charge_automatically' | 'send_invoice' | null — how the next auto-fire will fund (PSP off-session charge vs. one-off invoice), derived from the customer’s collection method.
  • currencyCreditAccountCurrency | null — the wallet’s currency metadata, or null when the customer hasn’t touched this currency yet. Use currency.denomination to pick the form variant without a second hook call.
  • denomination'unit' | 'usd' | … | null — convenience accessor for currency?.denomination.
  • set(input)(input: SetAutoTopUpInput) => Promise<AutoTopUpRule> — upserts the rule (auto-creates the credit account if missing). input is a discriminated union on mode (see below).
  • disable()() => Promise<void> — soft-disables the rule, keeping the rest of the config so a later re-enable pre-fills.
  • isMutatingtrue while a set() or disable() is in flight.
set(input) fields:
mode
'package' | 'amount'
required
'package' for unit-denominated wallets (buy a fixed SKU); 'amount' for fiat wallets (refill a free-form $ amount).
creditPackageId
string
required
mode: 'package' only. The SKU to auto-purchase when the balance drops below threshold.
topupAmount
number
required
mode: 'amount' only. Amount to add per fire, in minor units of the fiat denomination.
threshold
number
required
Balance (minor units) at or below which the rule fires.
monthlyChargeLimit
number
Optional cap (minor units) on total auto-topup spend per calendar month. null to clear.
enabled
boolean
Optional. Defaults to enabled on upsert; set false to stage a disabled rule.
import { useAutoTopUp } from '@unitpay/react';

export default function AutoTopUpToggle({ creditCurrencyId }: { creditCurrencyId: string }) {
  const { rule, denomination, set, disable, isMutating } = useAutoTopUp(creditCurrencyId);

  const enable = () =>
    denomination === 'unit'
      ? set({ mode: 'package', creditPackageId: 'cpkg_123', threshold: 1000 })
      : set({ mode: 'amount', topupAmount: 2000, threshold: 1000, monthlyChargeLimit: 50000 });

  return rule?.enabled ? (
    <button onClick={() => disable()} disabled={isMutating}>
      Disable auto-topup
    </button>
  ) : (
    <button onClick={enable} disabled={isMutating}>
      Enable auto-topup
    </button>
  );
}

useTopupHistory

Reads the customer’s top-up attempt log — manual, auto, and sweeper-replay attempts unified in one append-only feed. This table is the runtime source of truth behind the auto-topup counters (last-fired, consecutive failures, monthly cap usage). Parameters
filters
TopupHistoryFilters
Optional filter object. All fields are optional; omit it for the full history.
filters.currencyId
string
Scope to a single wallet currency.
filters.status
'pending' | 'succeeded' | 'failed' | 'skipped'
Filter by attempt status.
filters.triggerSource
'auto' | 'manual' | 'sweeper_replay'
Filter by what triggered the attempt.
filters.limit
number
Page size. The server caps at 100.
filters.cursor
string
Cursor from a previous page’s nextCursor.
Returns
  • attemptsTopupAttempt[] — the matching attempts (newest first), or [] while loading. Each row carries status, triggerSource, fundingMethod, amount / chargeAmount, balanceAtFire, thresholdAtFire, optional invoiceId / paymentId, and errorCode / errorMessage / skipReason on non-success.
  • nextCursorstring | null — pass to filters.cursor to fetch the next page, or null when none remain.
import { useTopupHistory } from '@unitpay/react';

export default function TopUpHistory({ currencyId }: { currencyId: string }) {
  const { attempts, isLoading, error } = useTopupHistory({ currencyId, limit: 20 });

  if (isLoading) return <p>Loading history…</p>;
  if (error) return <p>Failed to load history.</p>;
  if (attempts.length === 0) return <p>No top-up attempts yet.</p>;

  return (
    <ul>
      {attempts.map((a) => (
        <li key={a.id}>
          {a.triggerSource} · {a.status} · {a.chargeAmount ?? a.amount ?? '—'}
          {a.errorMessage ? ` — ${a.errorMessage}` : ''}
        </li>
      ))}
    </ul>
  );
}

useCreditLedger

Reads the customer’s credit ledger — the append-only record of grant, consumption, expiry, and adjustment rows. Backed by an infinite query: pages accumulate, fetchNextPage walks the server cursor for you, and changing a filter resets pages cleanly. The caller never threads cursor state. Parameters
filters
CreditLedgerFilters
Optional filter object. All fields are optional; omit it to read the full ledger.
filters.creditAccountId
string
Scope to one wallet / credit account.
filters.creditCurrencyId
string
Scope to one currency (e.g. AI Credits) when the customer has many wallets.
filters.type
string
Filter by the entry typecredit, debit, hold, release, expire, or adjustment (matched case-insensitively). This is the type column, not the more specific reason.
filters.featureId
string
Source feature filter — narrows to consumption rows debited by one feature.
filters.createdAfter
string
ISO datetime — inclusive lower bound on createdAt.
filters.createdBefore
string
ISO datetime — inclusive upper bound on createdAt.
filters.limit
number
Page size. The server caps at 100.
Returns
  • entriesCreditLedgerEntry[] — flattened entries across all loaded pages, or [] while loading. Each entry’s type is 'CREDIT' (balance up) or 'DEBIT' (balance down) — plus 'HOLD' / 'RELEASE' / 'EXPIRE' / 'ADJUSTMENT' — and a more specific reason (e.g. 'GRANT', 'USAGE_DEDUCTION', 'PACKAGE_PURCHASE', 'EXPIRATION'), with amount and the balanceBefore / balanceAfter snapshot.
  • fetchNextPage() — loads the next page and appends it to entries. No-op when hasNextPage is false.
  • hasNextPagebooleantrue when more pages remain on the server.
  • isFetchingNextPagebooleantrue while a subsequent page fetch is in flight.
import { useCreditLedger } from '@unitpay/react';

export default function LedgerDrawer({ creditCurrencyId }: { creditCurrencyId: string }) {
  const { entries, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } =
    useCreditLedger({ creditCurrencyId, limit: 25 });

  if (isLoading) return <p>Loading ledger…</p>;
  if (error) return <p>Failed to load ledger.</p>;

  return (
    <div>
      <ul>
        {entries.map((e) => (
          <li key={e.id}>
            {e.type === 'CREDIT' ? '+' : '-'}
            {e.amount} · {e.reason} · balance {e.balanceAfter}
          </li>
        ))}
      </ul>
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading…' : 'Load more'}
        </button>
      )}
    </div>
  );
}

See also

Credits overview

How grants, priority, FIFO consumption, and denominations work under the hood.

Entitlements & gates

Metered remaining and feature access — distinct from wallet balance.

Usage & analytics

Credit burn-rate and runway derived from the ledger.

Checkout

Preview a top-up or package purchase before charging.