> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useunitpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Credits & wallets

> Read wallet balances and grants, top up by amount or package, and automate refills — for the customer in context.

import LearnHooksTip from '/snippets/learn-hooks-tip.mdx';

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.

<LearnHooksTip />

Every hook returns `isLoading: boolean` and `error: Error | null` (see [Introduction](/react/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](/react/entitlements-and-gates) 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**

<ParamField body="customerId" type="string">
  Optional. Defaults to the customer set on `<UnitPayProvider>`. Pass an explicit id only to read a different customer's wallets.
</ParamField>

**Returns**

* `accounts` — `CreditAccount[]` — 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.

```tsx theme={null}
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>
  );
}
```

<Expandable title="Credit account object">
  ```json theme={null}
  {
    "id": "cacc_01k2v8...",
    "customerId": "cus_01ktx...",
    "creditCurrencyId": "ccur_01k0ai...",
    "status": "active",
    "balance": 4250,
    "currency": {
      "id": "ccur_01k0ai...",
      "slug": "ai-credits",
      "name": "AI Credits",
      "denomination": "unit",
      "minorUnitScale": 0
    },
    "grants": [
      {
        "id": "cgr_01k2...",
        "creditAccountId": "cacc_01k2v8...",
        "customerId": "cus_01ktx...",
        "creditCurrencyId": "ccur_01k0ai...",
        "subscriptionId": "sub_01k1...",
        "planId": "plan_01k0...",
        "creditPackageId": null,
        "sourceInvoiceId": null,
        "amount": 5000,
        "creditsRemaining": 4250,
        "priority": 10,
        "description": null,
        "expiresAt": "2026-07-01T00:00:00.000Z",
        "voidedAt": null,
        "metadata": { "source": "grant_rule", "trigger": "renewal", "periodKey": "2026-06" },
        "createdAt": "2026-06-01T00:00:00.000Z",
        "updatedAt": "2026-06-20T12:00:00.000Z"
      }
    ],
    "autoTopup": {
      "enabled": true,
      "threshold": 1000,
      "packageId": "cpkg_01k3...",
      "amount": null,
      "monthlyChargeLimit": 50000
    },
    "autoTopupRuntime": {
      "lastFiredAt": "2026-06-18T09:12:00.000Z",
      "lastFailedAt": null,
      "consecutiveFailures": 0,
      "totalFiresCount": 3,
      "isPending": false,
      "monthToDateCharge": 3000
    },
    "effectiveFundingMethod": "charge_automatically"
  }
  ```
</Expandable>

## 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**

* `currencies` — `CreditCurrency[]` — every active credit currency configured on the org, or `[]` while loading.

```tsx theme={null}
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>
  );
}
```

<Note>
  The first `useAutoTopUp` `set()` or first `useTopUp` `topUp()` call on an unengaged currency auto-creates the `credit_account` server-side.
</Note>

## 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`](/react/settlement) you wire to your UX with `on*` callbacks.

**Parameters** — `useTopUp(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:

<ParamField body="creditCurrencyId" type="string" required>
  The wallet currency to refill. Auto-creates the `credit_account` server-side if the customer hasn't engaged this currency yet.
</ParamField>

<ParamField body="amountCents" type="number" required>
  Amount to add, in minor units of the currency's denomination (`unit` credits or fiat).
</ParamField>

<ParamField body="idempotencyKey" type="string">
  Override the auto-generated `Idempotency-Key`. Auto-generated by default.
</ParamField>

<ParamField body="forceForm" type="boolean">
  Force the inline `<PaymentForm>` even when a payment method is on file.
</ParamField>

<Note>
  PLG customers with **no** payment method throw `TOPUP_REQUIRES_PM` (402) — route the customer to payment-method setup first.
</Note>

**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`).

```tsx theme={null}
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`](/react/settlement) 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.

**Parameters** — `useBuyCreditPackage(options?)` accepts an optional `HandleSettlementOptions` object of settlement callbacks (same set as `useTopUp`). The returned `buyCreditPackage(input)` alias takes:

<ParamField body="creditPackageId" type="string" required>
  The credit package SKU to purchase.
</ParamField>

<ParamField body="idempotencyKey" type="string">
  Override the auto-generated `Idempotency-Key`. Auto-generated by default.
</ParamField>

<ParamField body="forceForm" type="boolean">
  Force the inline `<PaymentForm>` even when a payment method is on file.
</ParamField>

<Note>
  PLG customers with **no** payment method throw — route the customer to payment-method setup first.
</Note>

**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.

```tsx theme={null}
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**

<ParamField body="creditCurrencyId" type="string" required>
  The wallet currency whose auto-topup rule to manage.
</ParamField>

**Returns**

* `rule` — `AutoTopupConfig | null` — current config (`enabled`, `threshold`, `packageId`, `amount`, `monthlyChargeLimit`), or `null` when no `credit_account` exists for this currency yet.
* `runtime` — `AutoTopupRuntime | 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.
* `currency` — `CreditAccountCurrency | 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.
* `isMutating` — `true` while a `set()` or `disable()` is in flight.

`set(input)` fields:

<ParamField body="mode" type="'package' | 'amount'" required>
  `'package'` for `unit`-denominated wallets (buy a fixed SKU); `'amount'` for fiat wallets (refill a free-form \$ amount).
</ParamField>

<ParamField body="creditPackageId" type="string" required>
  **`mode: 'package'` only.** The SKU to auto-purchase when the balance drops below `threshold`.
</ParamField>

<ParamField body="topupAmount" type="number" required>
  **`mode: 'amount'` only.** Amount to add per fire, in minor units of the fiat denomination.
</ParamField>

<ParamField body="threshold" type="number" required>
  Balance (minor units) at or below which the rule fires.
</ParamField>

<ParamField body="monthlyChargeLimit" type="number">
  Optional cap (minor units) on total auto-topup spend per calendar month. `null` to clear.
</ParamField>

<ParamField body="enabled" type="boolean">
  Optional. Defaults to enabled on upsert; set `false` to stage a disabled rule.
</ParamField>

```tsx theme={null}
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**

<ParamField body="filters" type="TopupHistoryFilters">
  Optional filter object. All fields are optional; omit it for the full history.
</ParamField>

<ParamField body="filters.currencyId" type="string">
  Scope to a single wallet currency.
</ParamField>

<ParamField body="filters.status" type="'pending' | 'succeeded' | 'failed' | 'skipped'">
  Filter by attempt status.
</ParamField>

<ParamField body="filters.triggerSource" type="'auto' | 'manual' | 'sweeper_replay'">
  Filter by what triggered the attempt.
</ParamField>

<ParamField body="filters.limit" type="number">
  Page size. The server caps at 100.
</ParamField>

<ParamField body="filters.cursor" type="string">
  Cursor from a previous page's `nextCursor`.
</ParamField>

**Returns**

* `attempts` — `TopupAttempt[]` — 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.
* `nextCursor` — `string | null` — pass to `filters.cursor` to fetch the next page, or `null` when none remain.

```tsx theme={null}
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**

<ParamField body="filters" type="CreditLedgerFilters">
  Optional filter object. All fields are optional; omit it to read the full ledger.
</ParamField>

<ParamField body="filters.creditAccountId" type="string">
  Scope to one wallet / credit account.
</ParamField>

<ParamField body="filters.creditCurrencyId" type="string">
  Scope to one currency (e.g. AI Credits) when the customer has many wallets.
</ParamField>

<ParamField body="filters.type" type="string">
  Filter by the entry `type` — `credit`, `debit`, `hold`, `release`, `expire`, or `adjustment` (matched case-insensitively). This is the `type` column, not the more specific `reason`.
</ParamField>

<ParamField body="filters.featureId" type="string">
  Source feature filter — narrows to consumption rows debited by one feature.
</ParamField>

<ParamField body="filters.createdAfter" type="string">
  ISO datetime — inclusive lower bound on `createdAt`.
</ParamField>

<ParamField body="filters.createdBefore" type="string">
  ISO datetime — inclusive upper bound on `createdAt`.
</ParamField>

<ParamField body="filters.limit" type="number">
  Page size. The server caps at 100.
</ParamField>

**Returns**

* `entries` — `CreditLedgerEntry[]` — 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`.
* `hasNextPage` — `boolean` — `true` when more pages remain on the server.
* `isFetchingNextPage` — `boolean` — `true` while a subsequent page fetch is in flight.

```tsx theme={null}
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>
  );
}
```

<Expandable title="Credit ledger entry object">
  ```json theme={null}
  {
    "id": "cle_01k4...",
    "customerId": "cus_01ktx...",
    "creditAccountId": "cacc_01k2v8...",
    "creditCurrencyId": "ccur_01k0ai...",
    "type": "DEBIT",
    "reason": "USAGE_DEDUCTION",
    "amount": 120,
    "balanceBefore": 4370,
    "balanceAfter": 4250,
    "grantId": "cgr_01k2...",
    "reservationId": null,
    "invoiceId": null,
    "featureId": "feat_01k9...",
    "usageEventId": "uev_01k9...",
    "metadata": {},
    "createdAt": "2026-06-20T12:00:00.000Z"
  }
  ```
</Expandable>

## See also

<CardGroup cols={2}>
  <Card title="Credits overview" icon="coins" href="/documentation/credits/overview">
    How grants, priority, FIFO consumption, and denominations work under the hood.
  </Card>

  <Card title="Entitlements & gates" icon="lock" href="/react/entitlements-and-gates">
    Metered `remaining` and feature access — distinct from wallet balance.
  </Card>

  <Card title="Usage & analytics" icon="chart-line" href="/react/usage-and-analytics">
    Credit burn-rate and runway derived from the ledger.
  </Card>

  <Card title="Checkout" icon="cart-shopping" href="/react/checkout">
    Preview a top-up or package purchase before charging.
  </Card>
</CardGroup>
