# Prepaid wallet

> How the prepaid wallet works: daily charges, top-ups with GST, the 7-day grace, the 402 wallet_suspended answer, the routes and the wallet.* events.

Canonical: https://docs.whabuzz.com/guides/billing/

WhaBuzz bills from a **prepaid wallet**. You add money with a top-up, and WhaBuzz takes a small amount from the balance every day for each connected number. There is no per-message fee. Meta's own charges go to your Meta account, not to the wallet (see [Pricing](https://docs.whabuzz.com/pricing.md)).

Every `/v1/billing` route answers `503 billing_disabled` while billing is switched off for the platform. Nothing is deducted until it is on.

## The numbers {#rates}

| | |
|---|---|
| First connected number | ₹500 per month |
| Each additional connected number | ₹250 per month |
| One day | the monthly price ÷ 30 |

All balances are whole **paise** and **exclude GST**: `150000` is ₹1,500.00.

The daily run happens at 02:30 IST. It charges every number that is `connected`; a number that is `pending` or `disconnected` is not charged. Sandbox accounts, trials, accounts billed through a connected app, and accounts WhaBuzz has waived are not charged (`billed: false` on the wallet, with the reason in `not_billed_reason`).

**Worked example.** Two connected numbers for four days:

```
4 days × (₹500 + ₹250) ÷ 30 = ₹100
```

That is ₹25.00 a day, so `daily_burn_paise` is `2500` and a balance of ₹1,845.00 has a `runway_days` of 73.

## GST and top-ups {#gst}

A top-up credits the amount you choose. **18% GST is added on top** and you pay the total:

| | |
|---|---|
| `amount_paise` | `150000`: credited to the wallet |
| `gst_paise` | `27000`: 18% of the amount, rounded to the paisa |
| `total_paise` | `177000`: what you pay |

A tax invoice from the same `WB/<financial year>/` series is issued when the payment is confirmed. With a GSTIN from another state it shows IGST; otherwise CGST and SGST. Set your GSTIN on **Billing > Settings** or with `PATCH /v1/tenant`.

The amount can be from `50000` (₹500, one month of one number) to `50000000` (₹5,00,000) paise. Below the minimum the answer is `400 amount_below_minimum` with `details.min_paise` (`50000`). You can top up from the day you sign up, including during the trial: the credit waits and is used once daily charges start (see [Trial](https://docs.whabuzz.com/pricing.md#trial)).

**The balance is non-refundable.** It is only returned, by hand, if you close the account.

## When the wallet runs out {#grace}

| State | What happens |
|---|---|
| `active` | Normal. `wallet.low` fires once when the runway drops to 7 days or less. |
| `grace` | The balance reached zero. **Sending keeps working for 7 days**, the balance goes negative, and `wallet.exhausted` fires every day with the days left. |
| `suspended` | The 7 days passed. Your numbers show `service_state: "suspended_billing"` and sends answer `402 wallet_suspended`. |

While suspended, **inbound messages and webhooks keep flowing**, and so do reads and mark-as-read. Only sending stops. A top-up that brings the balance above zero restores everything at once (`wallet.restored`). No daily charge is taken while suspended. The negative balance (the days used in grace) stays, and a top-up pays it off first, so a top-up smaller than that balance does not restore sending.

### The 402 {#402}

`POST /v1/messages` and `POST /v1/messages/batch` refuse before anything is queued:

```json
{
  "source": "whabuzz",
  "error": {
    "code": "wallet_suspended",
    "message": "Sending is paused: the prepaid wallet ran out and the 7-day grace period has ended. Top up the wallet to resume.",
    "details": { "top_up": "/v1/billing/wallet/top-ups" }
  },
  "request_id": "req_01JAB41B6E9G2J5M8P1S4V7Y0B"
}
```

Do not retry a `402 wallet_suspended`. Top up, then send again.

## Routes {#routes}

Reads need any access to the tenant. Starting a top-up needs an owner or admin session, or a key that can manage billing; anyone else gets `403`.

### Read the wallet {#wallet}

```bash
curl https://api.whabuzz.com/v1/billing/wallet \
  -H "Authorization: Bearer $WHABUZZ_KEY"
```

```json
{
  "currency": "INR",
  "balance_paise": 184500,
  "state": "active",
  "billed": true,
  "not_billed_reason": null,
  "billable_numbers": 2,
  "monthly_paise": 75000,
  "daily_burn_paise": 2500,
  "runway_days": 73,
  "rates": { "first_number_monthly_paise": 50000, "additional_number_monthly_paise": 25000 },
  "grace": null,
  "last_top_up": { "id": "wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD", "amount_paise": 150000, "total_paise": 177000, "paid_at": "2026-10-17T10:02:00Z", "invoice_number": "WB/2026-27/000001" },
  "next_deduction_at": "2026-10-22T21:00:00.000Z"
}
```

`runway_days` is `null` when nothing is being charged. In grace, `grace` is `{ "started_at", "ends_at", "days_left" }`. `next_deduction_at` is `null` until daily charges have begun.

### The ledger {#ledger}

Newest first. `limit` is 1 to 200 (default 50). Pass the `next_cursor` of one page as `cursor` for the next; it is `null` on the last page.

```bash
curl "https://api.whabuzz.com/v1/billing/wallet/ledger?limit=2" \
  -H "Authorization: Bearer $WHABUZZ_KEY"
```

```json
{
  "data": [
    { "id": "wl_01J9ZK3Q8M2V5N7R4T6X0YABCE", "kind": "debit", "amount_paise": 2500, "balance_after_paise": 184500, "currency": "INR", "ref_kind": "day", "ref_id": "2026-10-22", "exempt": false, "detail": { "numbers": 2 }, "created_at": "2026-10-22T21:00:04Z" },
    { "id": "wl_01J9ZK3Q8M2V5N7R4T6X0YABCF", "kind": "credit", "amount_paise": 150000, "balance_after_paise": 187000, "currency": "INR", "ref_kind": "payment", "ref_id": "wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD", "exempt": false, "detail": null, "created_at": "2026-10-17T10:02:00Z" }
  ],
  "next_cursor": "wl_01J9ZK3Q8M2V5N7R4T6X0YABCF"
}
```

`kind` is `credit`, `debit` or `adjustment`. `amount_paise` is always positive; the kind gives the direction.

### Start a top-up {#top-up}

```bash
curl -X POST https://api.whabuzz.com/v1/billing/wallet/top-ups \
  -H "Authorization: Bearer $WHABUZZ_KEY" \
  -H "Idempotency-Key: topup-2026-10-22-a" \
  -H "Content-Type: application/json" \
  -d '{ "amount_paise": 150000 }'
```

```json
{
  "id": "wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD",
  "state": "pending",
  "currency": "INR",
  "amount_paise": 150000,
  "gst_paise": 27000,
  "total_paise": 177000,
  "checkout_url": "https://pay.example/c/ord_1",
  "expires_at": "2026-10-22T11:00:00Z",
  "invoice_number": null,
  "failure_reason": null,
  "created_at": "2026-10-22T10:30:00Z",
  "paid_at": null
}
```

Send the person to `checkout_url` (UPI, card or netbanking). When they finish, the payment page returns them to `${CONSOLE_ORIGIN}/billing?top_up=<id>`; the wallet is credited when the payment is confirmed, usually within a minute.

The body takes `amount_paise` (whole paise) and an optional `phone`, the payer's 10-digit mobile for the receipt. It is asked for once and saved as your billing phone; you also need a billing email on the account. Without them the answer is `400 invalid_request`.

### List and read top-ups {#top-ups}

```bash
curl "https://api.whabuzz.com/v1/billing/wallet/top-ups?limit=25" -H "Authorization: Bearer $WHABUZZ_KEY"
curl https://api.whabuzz.com/v1/billing/wallet/top-ups/wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD -H "Authorization: Bearer $WHABUZZ_KEY"
```

The list answers `{ "data": [ … ] }`, newest first, with the same fields as above. `state` is `pending`, `paid`, `failed` or `expired`; `checkout_url` is only present while `pending`. Poll the single top-up after a payment until it leaves `pending`.

### The tax invoice {#invoice}

Ask the same route for HTML. It answers `409 conflict` until the top-up is paid.

```bash
curl https://api.whabuzz.com/v1/billing/wallet/top-ups/wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD \
  -H "Authorization: Bearer $WHABUZZ_KEY" -H "Accept: text/html" -o invoice.html
```

## Idempotency {#idempotency}

Send an `Idempotency-Key` (1 to 255 characters) when you start a top-up so a retry never opens a second checkout.

| Situation | Result |
|---|---|
| New key | `201` and a new top-up. |
| Same key, same body | `200` with the same top-up (and its open checkout). |
| Same key, different body | `422 idempotency_conflict`. Use a new key. |

The same rule applies to the top-up's checkout: it is created once per top-up, so replays return the same `checkout_url`.

## Events {#events}

The wallet raises six [events](https://docs.whabuzz.com/events/whabuzz-events.md#wallet). Subscribe to `wallet.*` types by name or with `*`. Each is deduplicated, so a retry never repeats one.

| Event | When |
|---|---|
| `wallet.credited` | A top-up was paid and credited. |
| `wallet.debited` | The day's charge, once per day, only when it is above zero. |
| `wallet.low` | The runway fell to 7 days or less. Once per crossing; it re-arms after a top-up. |
| `wallet.exhausted` | The balance reached zero (grace starts), then every day of grace. |
| `wallet.suspended` | The grace ended; sends now answer 402. |
| `wallet.restored` | The balance is positive again after grace or suspension. |

Example payloads (`data`):

```json
// wallet.credited
{ "amount_paise": 150000, "gst_paise": 27000, "total_paise": 177000, "balance_paise": 334500, "currency": "INR", "top_up_id": "wtu_01J9ZK3Q8M2V5N7R4T6X0YABCD", "invoice_number": "WB/2026-27/000001" }

// wallet.debited
{ "amount_paise": 2500, "balance_paise": 184500, "currency": "INR", "day": "2026-10-22", "numbers": 2 }

// wallet.low
{ "balance_paise": 12500, "currency": "INR", "runway_days": 5, "daily_burn_paise": 2500, "threshold_days": 7 }

// wallet.exhausted
{ "balance_paise": -3000, "currency": "INR", "grace_ends_at": "2026-10-27T21:00:00.000Z", "grace_days_left": 5 }

// wallet.suspended
{ "balance_paise": -20000, "currency": "INR", "grace_started_at": "2026-10-20T00:00:00.000Z" }

// wallet.restored
{ "balance_paise": 98000, "currency": "INR", "previous_state": "suspended" }
```

## In the console {#console}

**Billing > Wallet** shows the balance, runway, daily charge and next deduction, the top-up form with the GST line before you pay, and the ledger. Owners and admins can top up; other members see the balance only. Top-ups and their tax invoices are listed on **Billing > Top-ups**.
