Guides
Billing
How the wallet, price book, ledger, plans, invoices and top-ups work, and what happens at zero balance.
Calls are paid from a prepaid wallet. Every wallet movement is a ledger entry, and prices come from a price book the operator configures.
In the dashboard: Settings → Billing. Reading the balance, plans and prices needs any role. Reading the ledger, invoices and payments needs admin. Changing anything (plan, top-up, tax details, alerts) needs owner. An API key acts as admin, so it can read billing records but cannot top up or change the plan.
Note: The current list prices are on the Pricing page. The rates that apply to your workspace are the ones in its price book: read them in Settings → Billing or with
GET /billing/price-book. Amounts in the examples below are examples.
Units
- The wallet holds major units of the billing currency (rupees, dollars).
- Rates and call costs (
total_cost,cost_breakdown) are in minor units: 100 minor units are one major unit. - The billing endpoints on this page return money in major units.
Balance
curl https://api.sevennadi.si/billing/balance \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"{
"balance": 42.5,
"currency": "INR",
"paid_by_parent": false,
"low_balance_threshold": 1.0,
"low": false,
"plan": {"id": "payg", "name": "Starter", "kind": "payg", "status": "active", "period": "2026-10",
"monthly_fee": 0.0, "concurrency_limit": 10, "included_minutes": 0.0,
"included_minutes_left": 0.0, "included_minutes_used": 0.0},
"payments": {"provider": "mock", "available": true, "min": 1.0, "max": 500000.0},
"tax": {"rate_percent": 0.0, "mode": "exclusive"}
}The values here are examples. paid_by_parent is true for a sub-account, which spends its parent's wallet.
How a call is priced
cost = minutes x ( platform fee
+ telephony rate for the provider and direction
+ transcriber rate + LLM rate + synthesizer rate )- Every rate is per minute. Billing is by the second.
- A component is charged only if the engine reported using it.
- The platform's own models (provider
local) are priced at zero in the book. - A call that never connected charges nothing.
GET /billing/price-book returns the rates that apply to your workspace, including any custom prices the operator gave you ("custom": true). GET /get_current_price quotes a per-minute total from the same book.
The ledger
Each wallet movement is one immutable row. The database refuses updates and deletes on it.
curl "https://api.sevennadi.si/billing/ledger?kind=call&from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"One entry looks like this:
{"id": 9912, "kind": "call", "amount": -0.03, "balance_after": 42.47, "currency": "INR",
"description": "Call to +91******0123, 42 s", "reference": "call:b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90",
"execution_id": "b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90", "payment_id": null, "invoice_id": null,
"minutes": 0.7, "included_minutes": 0.0, "created_at": "2026-10-13T09:31:05Z"}kind | Meaning |
|---|---|
call | A finished call |
number_rental | A phone number's monthly rental |
plan_fee | A monthly plan's fee |
tax | Tax on an invoice (exclusive mode) |
setup_assistant | A setup assistant message, if the operator priced it (free in the shipped price book) |
topup, credit | Money added |
refund, adjustment, opening_balance | Corrections by the operator |
Filters: kind, execution_id, from, to; format=csv downloads the result; page_size up to 200.
Each entry has a unique reference. Posting the same thing twice changes nothing, so a call cannot be charged twice however often its job is retried. GET /billing/usage?days=30 returns spend, minutes and calls per day, built from the ledger.
Plans
GET /billing/plans lists the plans in the price book and your current one. Plans come in three kinds:
| Kind | Behaviour |
|---|---|
trial | What new sign-ups start on: the sign-up credit and a small concurrency limit |
payg | No monthly fee; calls are charged per minute |
monthly | A monthly fee, included call minutes and a higher concurrency limit; extra minutes at per-minute rates |
curl -X POST https://api.sevennadi.si/billing/plan \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"plan_id": "payg"}'Only plans marked self_serve can be chosen this way. Choosing a plan sets your concurrency limit. A monthly plan's fee is debited immediately and then each month; without enough balance the plan is past_due and gives no included minutes until a top-up lets the fee through.
Top-ups
curl -X POST https://api.sevennadi.si/billing/topups \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"amount": 500}'Response, 201:
{"id": "2f1e0d9c-8b7a-4f6e-9d5c-4b3a2f1e0d9c", "provider": "mock", "amount": 500.0, "currency": "INR",
"status": "paid", "checkout_url": null, "refunded_amount": 0.0, "error": null,
"created_at": "2026-10-13T11:00:00Z", "paid_at": "2026-10-13T11:00:00Z", "balance": 542.5}With a real payment provider the status is created and checkout_url is where to send the payer. The wallet is credited when the provider's webhook confirms the payment: the signature is verified, the amount and currency must match, and the credit is posted once. A receipt is emailed.
The default provider is mock, which pays at once. It is for development and is refused in production. amount must be within the operator's minimum and maximum. A sub-account cannot top up: A sub-account spends its parent's wallet: add funds on the parent account.
Invoices
A daily job charges plan fees and number rentals for the month that has started, then issues last month's invoice for every workspace with billable usage. Owners are emailed.
GET /billing/invoiceslists them;GET /billing/invoices/{invoice_id}/pdfdownloads one.- Tax uses a configured rate with a split by state code. In
exclusivemode the invoice's tax is debited from the wallet as its own ledger entry.
Low balance and zero balance
- One email when the balance drops under your threshold, and one when it runs out. No more until a top-up.
PUT /billing/alertswith{"low_balance_threshold": 100}sets your own threshold;0turns the low-balance email off;nullreturns to the platform default.- A single call with no balance ends
balance-low. - A batch at zero balance keeps
status: runningwithpaused_reason: "balance-low". Its calls stay queued and continue after a top-up. - A workflow campaign becomes
pausedwith the same reason and resumes after a top-up.
Limitations
- No payment has gone through the Razorpay or Stripe adapters; they have only been tested against local fakes.
- Invoices have not been reviewed by an accountant. There are no credit notes and no e-invoicing, and the PDF cannot print non-Latin scripts.
- A call is not cut off when the balance runs out mid-call, so the wallet can go below zero. Exclusive tax can also take it negative.
- A phone number is not released when the wallet cannot pay its rental.
For every endpoint see Billing reference.