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

# Wallet Management

> Create and manage customer wallets, credit and debit balances, view transaction history, and provision virtual bank accounts for automatic top-ups.

Customer wallets are prepaid balances that Monigo can debit at billing time (see [Prepaid Billing](/guides/prepaid-billing)) or that you can charge programmatically for any purpose. Every wallet operation is recorded with double-entry ledger entries for full auditability.

## Core concepts

| Concept             | Description                                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Wallet**          | A balance in a single currency belonging to one customer. A customer can have multiple wallets (one per currency).                                |
| **Ledger entry**    | One side of a double-entry accounting record. Every credit/debit creates exactly two entries — one debit and one credit — to keep books balanced. |
| **Virtual account** | A dedicated bank account (via Paystack, Flutterwave, or Monnify) that automatically credits the wallet when funds are deposited.                  |

***

## Creating a wallet

Use `getOrCreate` to ensure a wallet exists for a customer + currency pair. If one already exists, it is returned; otherwise a new wallet with zero balance is created.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.monigo.co/v1/wallets \
    -H "Authorization: Bearer mk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "customer_id": "cust_uuid",
      "currency": "NGN"
    }'
  ```

  ```go Go theme={null}
  wallet, err := client.Wallets.GetOrCreate(ctx, monigo.GetOrCreateWalletRequest{
      CustomerID: "cust_uuid",
      Currency:   "NGN",
  })
  fmt.Printf("Wallet %s — balance: %s %s\n", wallet.ID, wallet.Balance, wallet.Currency)
  ```

  ```ts TypeScript theme={null}
  const wallet = await client.wallets.getOrCreate({
    customer_id: "cust_uuid",
    currency: "NGN",
  });
  console.log(`Wallet ${wallet.id} — balance: ${wallet.balance} ${wallet.currency}`);
  ```
</CodeGroup>

***

## Listing wallets

### All wallets

The org is inferred from your API key.

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.List(ctx)
  for _, w := range resp.Wallets {
      fmt.Printf("  %s — %s %s\n", w.CustomerID, w.Balance, w.Currency)
  }
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.list();
  for (const w of resp.wallets) {
    console.log(`  ${w.customer_id} — ${w.balance} ${w.currency}`);
  }
  ```
</CodeGroup>

### Filter by customer

You can pass an optional `customer_id` query parameter to filter wallets:

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.List(ctx, monigo.ListWalletsParams{
      CustomerID: "cust_uuid",
  })
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.list({ customer_id: "cust_uuid" });
  ```
</CodeGroup>

Or use the dedicated customer wallets endpoint:

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.ListByCustomer(ctx, "cust_uuid")
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.listByCustomer("cust_uuid");
  ```
</CodeGroup>

***

## Getting a wallet

Fetching a single wallet also returns its virtual accounts.

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.Get(ctx, "wallet_uuid")
  fmt.Printf("Balance: %s\n", resp.Wallet.Balance)
  fmt.Printf("Virtual accounts: %d\n", len(resp.VirtualAccounts))
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.get("wallet_uuid");
  console.log(`Balance: ${resp.wallet.balance}`);
  console.log(`Virtual accounts: ${resp.virtual_accounts.length}`);
  ```
</CodeGroup>

***

## Crediting a wallet

Credit adds funds to the wallet. Every credit creates two ledger entries (debit provider, credit wallet) and fires a `customer.wallet.topped_up` webhook.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.monigo.co/v1/wallets/{wallet_id}/credit \
    -H "Authorization: Bearer mk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "amount": "10000.00",
      "currency": "NGN",
      "description": "Top-up via payment link",
      "entry_type": "deposit",
      "reference_type": "payment_link",
      "reference_id": "pay_abc123",
      "idempotency_key": "topup_abc123"
    }'
  ```

  ```go Go theme={null}
  resp, err := client.Wallets.Credit(ctx, walletID, monigo.CreditWalletRequest{
      Amount:         "10000.00",
      Currency:       "NGN",
      Description:    "Top-up via payment link",
      EntryType:      monigo.WalletEntryTypeDeposit,
      ReferenceType:  "payment_link",
      ReferenceID:    "pay_abc123",
      IdempotencyKey: "topup_abc123",
  })
  fmt.Printf("New balance: %s\n", resp.Wallet.Balance)
  ```

  ```ts TypeScript theme={null}
  import { WalletEntryType } from "@monigo/sdk";

  const resp = await client.wallets.credit(walletId, {
    amount: "10000.00",
    currency: "NGN",
    description: "Top-up via payment link",
    entry_type: WalletEntryType.Deposit,
    reference_type: "payment_link",
    reference_id: "pay_abc123",
    idempotency_key: "topup_abc123",
  });
  console.log(`New balance: ${resp.wallet.balance}`);
  ```
</CodeGroup>

<Tip>
  Always provide an `idempotency_key` when crediting wallets. This prevents double-credits if the request is retried. Use a stable reference from your own system (payment ID, transfer reference, etc.).
</Tip>

### Entry types for credits

| Constant                                                   | Value        | Use case                          |
| ---------------------------------------------------------- | ------------ | --------------------------------- |
| `WalletEntryTypeDeposit` / `WalletEntryType.Deposit`       | `deposit`    | Customer top-up, payment received |
| `WalletEntryTypeRefund` / `WalletEntryType.Refund`         | `refund`     | Reversing a previous charge       |
| `WalletEntryTypeAdjustment` / `WalletEntryType.Adjustment` | `adjustment` | Manual balance correction         |

***

## Debiting a wallet

Debit removes funds from the wallet. Returns a **402 Payment Required** error if the balance is insufficient.

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.Debit(ctx, walletID, monigo.DebitWalletRequest{
      Amount:         "500.00",
      Currency:       "NGN",
      Description:    "Usage charge for March 2026",
      EntryType:      monigo.WalletEntryTypeUsage,
      ReferenceType:  "invoice",
      ReferenceID:    "inv_abc123",
      IdempotencyKey: "debit_inv_abc123",
  })
  if monigo.IsQuotaExceeded(err) {
      // 402 — insufficient balance
      log.Println("Wallet balance too low")
  }
  ```

  ```ts TypeScript theme={null}
  import { MonigoAPIError, WalletEntryType } from "@monigo/sdk";

  try {
    const resp = await client.wallets.debit(walletId, {
      amount: "500.00",
      currency: "NGN",
      description: "Usage charge for March 2026",
      entry_type: WalletEntryType.Usage,
      reference_type: "invoice",
      reference_id: "inv_abc123",
      idempotency_key: "debit_inv_abc123",
    });
    console.log(`New balance: ${resp.wallet.balance}`);
  } catch (err) {
    if (err instanceof MonigoAPIError && err.statusCode === 402) {
      console.log("Wallet balance too low");
    }
  }
  ```
</CodeGroup>

### Entry types for debits

| Constant                                                   | Value        | Use case                  |
| ---------------------------------------------------------- | ------------ | ------------------------- |
| `WalletEntryTypeUsage` / `WalletEntryType.Usage`           | `usage`      | Metered usage charge      |
| `WalletEntryTypeWithdrawal` / `WalletEntryType.Withdrawal` | `withdrawal` | Customer withdrawal       |
| `WalletEntryTypeAdjustment` / `WalletEntryType.Adjustment` | `adjustment` | Manual balance correction |

***

## Transaction history

List paginated ledger entries for a wallet.

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.ListTransactions(ctx, walletID, monigo.ListTransactionsParams{
      Limit:  25,
      Offset: 0,
  })
  for _, tx := range resp.Transactions {
      fmt.Printf("  %s %s %s — %s (%s → %s)\n",
          tx.Direction, tx.Amount, tx.Currency,
          tx.Description, tx.BalanceBefore, tx.BalanceAfter)
  }
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.listTransactions(walletId, {
    limit: 25,
    offset: 0,
  });
  for (const tx of resp.transactions) {
    console.log(
      `  ${tx.direction} ${tx.amount} ${tx.currency} — ${tx.description} ` +
      `(${tx.balance_before} → ${tx.balance_after})`
    );
  }
  ```
</CodeGroup>

***

## Virtual accounts

Virtual accounts are dedicated bank accounts (via Paystack, Flutterwave, or Monnify) that automatically credit the wallet when a customer deposits funds. This enables self-service top-ups without requiring API calls from your backend.

### Create a virtual account

<CodeGroup>
  ```go Go theme={null}
  va, err := client.Wallets.CreateVirtualAccount(ctx, walletID, monigo.CreateVirtualAccountRequest{
      Provider: monigo.VirtualAccountProviderPaystack,
      Currency: "NGN",
  })
  fmt.Printf("Account: %s at %s (%s)\n", va.AccountNumber, va.BankName, va.AccountName)
  ```

  ```ts TypeScript theme={null}
  import { VirtualAccountProvider } from "@monigo/sdk";

  const va = await client.wallets.createVirtualAccount(walletId, {
    provider: VirtualAccountProvider.Paystack,
    currency: "NGN",
  });
  console.log(`Account: ${va.account_number} at ${va.bank_name} (${va.account_name})`);
  ```
</CodeGroup>

### List virtual accounts

<CodeGroup>
  ```go Go theme={null}
  resp, err := client.Wallets.ListVirtualAccounts(ctx, walletID)
  for _, va := range resp.VirtualAccounts {
      fmt.Printf("  %s — %s %s (%s)\n", va.Provider, va.AccountNumber, va.BankName, va.Currency)
  }
  ```

  ```ts TypeScript theme={null}
  const resp = await client.wallets.listVirtualAccounts(walletId);
  for (const va of resp.virtual_accounts) {
    console.log(`  ${va.provider} — ${va.account_number} ${va.bank_name} (${va.currency})`);
  }
  ```
</CodeGroup>

| Provider constant                                                          | Value         |
| -------------------------------------------------------------------------- | ------------- |
| `VirtualAccountProviderPaystack` / `VirtualAccountProvider.Paystack`       | `paystack`    |
| `VirtualAccountProviderFlutterwave` / `VirtualAccountProvider.Flutterwave` | `flutterwave` |
| `VirtualAccountProviderMonnify` / `VirtualAccountProvider.Monnify`         | `monnify`     |

***

## Prepaid billing integration

Wallets are the foundation of [Prepaid Billing](/guides/prepaid-billing). When a customer subscribes to a prepaid plan:

1. Monigo auto-creates a wallet in the plan's currency
2. At period-end, Monigo debits the wallet atomically and creates a `paid` invoice
3. If the balance is insufficient, the subscription is paused and a `subscription.prepaid_balance_insufficient` webhook fires
4. When you credit the wallet and the balance covers the outstanding invoice, Monigo auto-resumes the subscription

This means you only need to handle wallet top-ups — Monigo takes care of billing, pausing, and resuming automatically.

### Real-time billing

A plan with `billing_mode: "realtime"` debits the wallet in near-real-time (within \~5 seconds) as usage accrues, rather than once at the end of the period. It supports every pricing model (`per_unit`, `tiered`, `volume`, `package`, `cap`): as events arrive, Monigo recomputes the cumulative charge for the period and debits the wallet for the marginal difference.

* **Continuous metering** — each new batch of usage triggers a reconcile that charges only the incremental amount, so the wallet always reflects what the customer owes so far.
* **Pause on empty** — when the wallet can no longer cover the next marginal charge, the subscription is paused, metering stops, and a `subscription.prepaid_balance_insufficient` webhook fires.
* **Auto-resume on top-up** — crediting the wallet reactivates the subscription, reconciles any usage that accrued while paused, and resumes metering automatically.
* **One paid summary invoice per period** — at period-end Monigo emits a single `paid` invoice that itemizes the period's usage per metric, with the total equal to what was already debited in real time. No additional debit happens at close — the invoice is purely a reporting artifact.

Realtime plans must be `collection` plans (a `realtime` `payout` plan is rejected). As with prepaid, you only need to handle wallet top-ups — metering, pausing, resuming, and the period-end invoice are all automatic.

***

## Inline funding session

When a customer wants to top up their own wallet directly in your portal UI (without a server-side redirect), set `"inline": true` in the funding request. Monigo returns a `WalletFundingSession` containing only publishable credentials — no secret keys are ever exposed.

<Warning>
  The provider webhook remains the source of truth for crediting the wallet. Do **not** credit the wallet client-side after the payment completes — Monigo handles that automatically when the provider sends its payment notification.
</Warning>

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.monigo.co/v1/portal/wallets/{wallet_id}/fund \
    -H "Authorization: Bearer pk_portal_..." \
    -H "Content-Type: application/json" \
    -d '{
      "amount": "5000.00",
      "currency": "NGN",
      "inline": true
    }'
  ```

  ```ts TypeScript theme={null}
  const session = await portalClient.wallets.fund(walletId, {
    amount: "5000.00",
    currency: "NGN",
    inline: true,
  });
  ```
</CodeGroup>

### Response — `WalletFundingSession`

```json theme={null}
{
  "provider": "paystack",
  "reference": "mng_wf_01j...",
  "amount": "5000.00",
  "currency": "NGN",
  "email": "customer@example.com",
  "customer_name": "Ada Lovelace",
  "public_key": "pk_live_...",
  "access_code": "0pepr...",
  "authorization_url": "https://checkout.paystack.com/0pepr..."
}
```

| Field               | Type   | Description                                                   |
| ------------------- | ------ | ------------------------------------------------------------- |
| `provider`          | string | Payment provider (`paystack`, `stripe`, `monnify`)            |
| `reference`         | string | Monigo-generated payment reference                            |
| `amount`            | string | Funding amount                                                |
| `currency`          | string | ISO 4217 currency code                                        |
| `email`             | string | Customer email address                                        |
| `customer_name`     | string | Customer display name                                         |
| `public_key`        | string | Provider **publishable** key (safe for the browser)           |
| `access_code`       | string | Paystack access code — pass to `PaystackPop.newTransaction()` |
| `client_secret`     | string | Stripe client secret — pass to Stripe.js `confirmPayment()`   |
| `contract_code`     | string | Monnify contract code — used to initialise the Monnify SDK    |
| `meta`              | object | Provider-specific extra fields                                |
| `authorization_url` | string | Fallback redirect URL (also populated for inline sessions)    |

### Using the session in the browser

Pass the session fields directly to your provider's inline SDK. Only the fields relevant to the active `provider` will be populated.

<CodeGroup>
  ```ts Paystack theme={null}
  const handler = PaystackPop.setup({
    key: session.public_key,
    email: session.email,
    amount: parseFloat(session.amount) * 100, // kobo
    ref: session.reference,
    access_code: session.access_code,
    onSuccess: () => { /* wallet credited by webhook — just refresh balance */ },
  });
  handler.openIframe();
  ```

  ```ts Stripe theme={null}
  const stripe = Stripe(session.public_key);
  await stripe.confirmPayment({
    clientSecret: session.client_secret,
    elements,
  });
  ```
</CodeGroup>

<Tip>
  Only **publishable** keys are returned in the inline session. Secret keys live exclusively on the Monigo backend and are never forwarded to the portal.
</Tip>

***

## Drop-in wallet widget (inline funding + live balance)

The `<WalletWidget>` component is a self-contained UI that shows the customer's live
balance and opens the configured payment provider's inline checkout when they click
**Fund wallet**. Drop it anywhere inside a `<MonigoProvider>` — no extra state
management required.

### Installation

```bash theme={null}
npm install @monigo/svelte @monigo/tokens
```

### Usage

<CodeGroup>
  ```svelte Svelte theme={null}
  <script>
    import { MonigoProvider, WalletWidget } from '@monigo/svelte'
    import '@monigo/tokens/monigo.css'

    // portalToken is the raw hex string from PortalToken.token — mint it server-side.
    let { portalToken, walletId, currency } = $props()
  </script>

  <MonigoProvider {portalToken}>
    <WalletWidget {walletId} {currency} />
  </MonigoProvider>
  ```

  ```tsx React theme={null}
  import { MonigoProvider, WalletWidget } from '@monigo/react'
  import '@monigo/tokens/monigo.css'

  export function AgentWallet({ portalToken, walletId, currency }) {
    return (
      <MonigoProvider portalToken={portalToken}>
        <WalletWidget walletId={walletId} currency={currency} />
      </MonigoProvider>
    )
  }
  ```

  ```vue Vue theme={null}
  <script setup>
  import { MonigoProvider, WalletWidget } from '@monigo/vue'
  import '@monigo/tokens/monigo.css'

  defineProps(['portalToken', 'walletId', 'currency'])
  </script>

  <template>
    <MonigoProvider :portal-token="portalToken">
      <WalletWidget :wallet-id="walletId" :currency="currency" />
    </MonigoProvider>
  </template>
  ```
</CodeGroup>

### Minting a portal token (server-side)

`MonigoProvider` expects the **raw token string** (`PortalToken.token`) — not the
full object returned by the SDK. Mint it on the server so your secret API key is never
exposed to the browser.

```ts TypeScript (SvelteKit server load) theme={null}
import { MonigoClient } from '@monigo/sdk'
import { env } from '$env/dynamic/private'

const monigo = new MonigoClient({ apiKey: env.MONIGO_API_KEY })

export const load = async () => {
  const portalToken = await monigo.portalTokens.create({
    customer_external_id: 'your-customer-external-id',
    label: 'Wallet widget session',
  })
  return { portalToken: portalToken.token }
}
```

<Tip>
  `portalTokens.create()` accepts a `customer_external_id` (the `external_id` you
  set when creating the customer) — **not** the internal Monigo UUID. The SDK infers
  the organisation from your API key.
</Tip>

### Props

| Prop             | Type                          | Default               | Description                                                                                                                  |
| ---------------- | ----------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `walletId`       | `string`                      | —                     | UUID of the wallet to display and fund. **Required.**                                                                        |
| `currency`       | `string`                      | —                     | ISO 4217 currency code (e.g. `NGN`). **Required.**                                                                           |
| `presets`        | `number[]`                    | `[1000, 5000, 10000]` | Quick-pick funding amounts shown as chips.                                                                                   |
| `pollIntervalMs` | `number`                      | `8000`                | How often (ms) the widget re-fetches the live balance.                                                                       |
| funded callback  | `(reference: string) => void` | —                     | Called with the provider payment reference after a successful top-up. Svelte: `onfunded`; React: `onFunded`; Vue: `@funded`. |
| error callback   | `(err: unknown) => void`      | —                     | Called when a balance fetch or checkout fails. Svelte: `onerror`; React: `onError`; Vue: `@error`.                           |
| `class`          | `string`                      | —                     | Extra CSS classes applied to the widget wrapper (Svelte/Vue; React uses `className`).                                        |

### What the widget does

* **Live balance** — polls `/portal/wallets/{walletId}` every `pollIntervalMs`
  milliseconds and formats the balance with `Intl.NumberFormat`.
* **Inline checkout** — calls the portal funding endpoint to get a
  `WalletFundingSession` and launches Paystack, Stripe, Flutterwave, or Monnify inline.
  Only the relevant provider fields are populated (see [Inline funding session](#inline-funding-session)).
* **Auto-refresh** — after a successful payment, the widget immediately polls for the
  updated balance, so the customer sees the new total without a page reload.
* **Usage debits** — as your backend debits the wallet for metered usage (via
  `wallets.debit()`), the polling loop picks up the reduced balance automatically.

<Warning>
  Never credit the wallet client-side after a payment completes. The provider webhook
  is the source of truth — Monigo credits the wallet automatically when the provider
  sends its payment notification.
</Warning>

### Full working example

See [`platform/samples/ai-agent-wallet`](../samples/ai-agent-wallet) for a complete
SvelteKit app that mints a portal token on the server, renders the widget, and
includes a form action that debits the wallet to simulate agent token usage.

***

## Webhook events

| Event                                       | When fired                                      |
| ------------------------------------------- | ----------------------------------------------- |
| `customer.wallet.topped_up`                 | Wallet credited successfully                    |
| `subscription.prepaid_balance_insufficient` | Prepaid billing failed — wallet balance too low |
| `invoice.paid`                              | Prepaid invoice paid via wallet debit           |

***

## Related

* [Prepaid Billing](/guides/prepaid-billing) — end-to-end prepaid subscription flow
* [Invoice Lifecycle](/guides/invoice-lifecycle) — invoice states and transitions
* [Subscription Lifecycle](/guides/subscription-lifecycle) — pause, resume, and cancel behavior
* [Webhooks](/integrations/webhooks) — receiving billing events
