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

# Customer Wallets (WaaS)

> Create a wallet, receive money via a virtual account, move it between wallets, and pay out to banks — the full customer-wallet lifecycle.

Each customer can hold one or more **wallets**. A wallet is usable for internal value the moment it exists — no virtual account required — and gains the ability to receive external money, pay out, and back a card as the customer clears each KYC tier.

All amounts are in the wallet's currency's major unit (e.g. Naira, not kobo). Every response uses the standard `{ status, message, code, version, data }` envelope.

## Create a wallet

```
POST /api/v1/customers/{customerId}/wallets
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **KYC:** tier 0

No NUBAN is issued — the wallet works for internal value immediately; attach a virtual account later.

```bash theme={null}
curl -X POST ".../api/v1/customers/4021/wallets" \
  -H "X-Numero-Api-Key: your_api_key" \
  -H "X-Numero-Signature: $SIGNATURE" \
  -H "X-Numero-Signature-Version: v2" \
  -d '{ "currency": "NGN" }'
```

```json theme={null}
{ "status": true, "code": "200", "data": {
  "walletReference": "cw_9f3a…", "customerId": 4021, "currency": "NGN",
  "ledgerBalance": 0, "availableBalance": 0, "pendingBalance": 0, "heldBalance": 0,
  "status": "Active", "virtualAccountNumber": null, "isTest": false, "createdAt": "2026-07-24T09:13:00Z" } }
```

Address wallets by their `walletReference` everywhere below.

## List a customer's wallets

```
GET /api/v1/customers/{customerId}/wallets
```

**Scope:** `customer_wallet:read`. Returns an array of the wallet view above.

## Read a wallet's balance

```
GET /api/v1/wallets/{walletReference}
```

**Scope:** `customer_wallet:read`. Returns the four balances:

| Field              | Meaning                            |
| ------------------ | ---------------------------------- |
| `ledgerBalance`    | Everything the wallet holds        |
| `availableBalance` | Spendable now (`ledger − held`)    |
| `pendingBalance`   | Inbound money awaiting settlement  |
| `heldBalance`      | Reserved against in-flight payouts |

## Attach a virtual account (receive external money)

```
POST /api/v1/wallets/{walletReference}/virtual-account
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **KYC:** tier 1 (verified BVN)

Assigns a NUBAN so the wallet can receive money from **outside** Numero. Below tier 1 this returns a `403` telling you to verify the customer's BVN first.

```json theme={null}
{ "status": true, "code": "200", "data": {
  "accountNumber": "1234567890", "bankName": "…", "accountName": "Ada Obi" } }
```

Incoming money lands in `pendingBalance` and settles to `availableBalance`.

Calling this again on a wallet that already has a NUBAN is **idempotent** — you get the existing account back rather than a second one being minted and charged for.

## Retire a virtual account

```
DELETE /api/v1/wallets/{walletReference}/virtual-account
```

**Signature required:** Yes · **Scope:** `customer_wallet:write`

Retires the wallet's active NUBAN. Use this when a customer closes their account or you need to stop advertising a number that has been shared too widely.

<Warning>
  **This is one-way.** There is no un-retire. The only route back to a working NUBAN on this wallet is [regenerate](#regenerate-a-virtual-account), which issues a **different** account number — the retired one never comes back.
</Warning>

```bash theme={null}
curl -X DELETE ".../api/v1/wallets/cw_9f3a…/virtual-account" \
  -H "X-Numero-Api-Key: your_api_key" \
  -H "X-Numero-Signature: $SIGNATURE" \
  -H "X-Numero-Signature-Version: v2"
```

```json theme={null}
{ "status": true, "code": "200", "data": true }
```

Money is never lost. A retired NUBAN is a **soft** decommission: anything that still arrives on it is credited to the wallet as normal, but raises an alert for our operations team — a signal that a payer somewhere is still using a number you have withdrawn.

Tell your customers before you retire. Anyone holding the old number — a saved beneficiary, a standing order, an invoice already sent — keeps paying into it, and each of those payments becomes an alert.

Returns `400` if the wallet has no active virtual account, so retiring twice is safe rather than silently successful.

## Regenerate a virtual account

```
POST /api/v1/wallets/{walletReference}/virtual-account/regenerate
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **KYC:** tier 1 (verified BVN)

Retires the current NUBAN and attaches a fresh one in a single call. Reach for this when a number has been exposed and you want the customer receiving on a clean one.

<Warning>
  **The account number changes.** Every payer who saved the old NUBAN is now paying a retired account. Treat regeneration as a customer-communication exercise, not just an API call — publish the new number everywhere the old one appears before you regenerate, not after.
</Warning>

```bash theme={null}
curl -X POST ".../api/v1/wallets/cw_9f3a…/virtual-account/regenerate" \
  -H "X-Numero-Api-Key: your_api_key" \
  -H "X-Numero-Signature: $SIGNATURE" \
  -H "X-Numero-Signature-Version: v2"
```

Returns the same shape as attach, carrying the **new** account number:

```json theme={null}
{ "status": true, "code": "200", "data": {
  "accountNumber": "0987654321", "bankName": "…", "accountName": "Ada Obi" } }
```

Safe on a wallet that has no virtual account yet — the retire step is skipped and you simply get a new NUBAN. The customer must still be at KYC tier 1, exactly as for attach.

## Transfer between wallets (internal, instant)

```
POST /api/v1/wallets/{walletReference}/transfer
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **Idempotency-Key:** required

Moves money to another wallet of the **same merchant** and currency. Pure sub-ledger move — instant, no external rail.

```bash theme={null}
curl -X POST ".../api/v1/wallets/cw_9f3a…/transfer" \
  -H "X-Numero-Api-Key: your_api_key" \
  -H "X-Numero-Signature: $SIGNATURE" \
  -H "X-Numero-Signature-Version: v2" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "toWalletReference": "cw_7b1c…", "amount": 2500, "narrative": "split" }'
```

Returns the **source** wallet's updated balances.

## Pay out to a bank

```
POST /api/v1/wallets/{walletReference}/payout
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **Idempotency-Key:** required · **KYC:** tier 2 (fully identified)

Sends money to an external bank account. Funds are reserved immediately; the payout returns in `Held` and settles when the bank transfer completes. Bounded by the customer's per-tier velocity limits.

```bash theme={null}
curl -X POST ".../api/v1/wallets/cw_9f3a…/payout" \
  -H "X-Numero-Api-Key: your_api_key" \
  -H "X-Numero-Signature: $SIGNATURE" \
  -H "X-Numero-Signature-Version: v2" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "amount": 5000, "destinationAccountNumber": "0123456789",
        "destinationBankCode": "058", "destinationAccountName": "Ada Obi", "narrative": "payout" }'
```

```json theme={null}
{ "status": true, "code": "200", "data": {
  "payoutReference": "po_2f8b…", "walletReference": "cw_9f3a…",
  "amount": 5000, "fee": 10, "currency": "NGN",
  "destinationAccountNumber": "0123456789", "destinationBankCode": "058",
  "status": "Held", "walletAvailableBalance": 12490, "createdAt": "2026-07-24T09:20:00Z" } }
```

## Statement

```
GET /api/v1/wallets/{walletReference}/transactions?pageNumber=1&pageSize=50
```

**Scope:** `customer_wallet:read`. Paginated ledger entries — each with `postingType`, `amount`, `balanceBefore`/`balanceAfter`, `narrative`, `transactionReference`, and `counterpartyWalletReference` for internal transfers.

## Link a card

```
POST /api/v1/wallets/{walletReference}/link-card
```

**Signature required:** Yes · **Scope:** `customer_wallet:write` · **KYC:** tier 2

Links the wallet to the customer's card profile so an issued card draws from the wallet balance. Because a card is external spending, it carries the same tier bar as a payout.
