API reference
Billing
Wallet balance, usage ledger, prices, plans, invoices, top-ups, tax details and alerts.
Calls are paid from a prepaid wallet. These endpoints read the balance, the ledger behind it, the prices and plans, and the monthly invoices, and they let an owner top up the wallet, change the plan and set tax details. The billing guide explains how a call is priced.
Reading the balance, plans and prices needs the viewer role. Other reads need admin. Every change needs the owner role. An API key acts as an admin, so it can read everything here but cannot top up, change the plan, or edit tax details and alerts.
| Operation | Method and path | Minimum role |
|---|---|---|
| Get the balance | GET /billing/balance | viewer |
| List ledger entries | GET /billing/ledger | admin |
| Get usage per day | GET /billing/usage | admin |
| Get the price book | GET /billing/price-book | viewer |
| List plans | GET /billing/plans | viewer |
| Choose a plan | POST /billing/plan | owner |
| List invoices | GET /billing/invoices | admin |
| Get an invoice | GET /billing/invoices/{invoice_id} | admin |
| Download an invoice PDF | GET /billing/invoices/{invoice_id}/pdf | admin |
| Start a top-up | POST /billing/topups | owner |
| List top-ups | GET /billing/topups | admin |
| Get a top-up | GET /billing/topups/{payment_id} | admin |
| Receive a payment webhook | POST /billing/webhooks/{provider} | none |
| Get tax details | GET /billing/tax-details | admin |
| Set tax details | PUT /billing/tax-details | owner |
| Get alert settings | GET /billing/alerts | admin |
| Set alert settings | PUT /billing/alerts | owner |
Units and what is simulated
- Major and minor units. The wallet is held in major units of the billing currency (for example rupees). Rates in the price book are in minor units per minute (100 minor = 1 major). The endpoints on this page return balances, ledger amounts, fees, invoice totals and top-up amounts in major units. The price book is the exception: its rates are minor units per minute, and it says so in its
unitfield. - Prices are placeholders. The rates and plans shipped in the price book are operator-configured placeholder values, not real prices. The operator must set real rates, the sign-up credit and the default low-balance threshold before going live.
- Payment providers. The default provider is
mock: it "pays" at once and exists for development and tests. It is refused in production. Adapters for Razorpay and Stripe exist, but they have only been tested against local fakes. No real payment has been made through either. - Tax. Invoices apply an operator-configured rate with a GST-style split (CGST and SGST when the seller's and buyer's state codes match, IGST otherwise). This is not tax advice. There are no credit notes and no e-invoicing integration.
Role errors
A caller below the required role gets 403:
Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace.
Your role (member) can't do this. It needs admin access; ask an owner or admin of the workspace.Balance and usage
Get the balance
GET /billing/balance
Returns the wallet balance, the current plan with its included minutes, which payment provider is configured, and the tax settings. A sub-account spends its parent's wallet, so it sees the parent's balance with paid_by_parent: true.
Auth: API key or session token. Role: viewer and up.
Request
curl https://api.sevennadi.si/billing/balance \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"balance": 5.0,
"currency": "INR",
"paid_by_parent": false,
"low_balance_threshold": 1.0,
"low": false,
"plan": {
"id": "trial",
"name": "Free trial",
"kind": "trial",
"status": "active",
"period": "2026-10",
"monthly_fee": 0.0,
"concurrency_limit": 5,
"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": 18.0, "mode": "exclusive"}
}| Field | Meaning |
|---|---|
balance, low_balance_threshold | Major units. low is true when the balance is under the threshold. |
plan.kind | trial, payg or monthly. |
plan.status | active, or past_due when a monthly fee could not be debited. |
plan.period | The billing month, YYYY-MM, in the operator's billing time zone. |
payments.available | Whether the configured provider has its credentials (for mock: whether this is not production). |
payments.min, payments.max | Smallest and largest single top-up, major units. |
tax.mode | exclusive (tax is debited on top, as its own ledger entry) or inclusive. |
The values shown are from a development setup; all of them are operator-configured.
List ledger entries
GET /billing/ledger
Returns the wallet's ledger, newest first. Every change to the wallet is one ledger entry that is never edited or deleted. A sub-account sees only the entries it caused on its parent's wallet.
Auth: API key or session token. Role: admin and up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
page_number | query | integer | no | From 1. Default 1. |
page_size | query | integer | no | 1 to 200. Default 50. |
kind | query | string | no | One or more kinds, comma-separated. See below. |
execution_id | query | string (UUID) | no | Only the entries of one call. |
from | query | string | no | ISO date or date-time; entries created at or after it. |
to | query | string | no | ISO date or date-time; entries created at or before it. |
format | query | string | no | json (default) or csv. CSV ignores paging and returns up to 50,000 rows as a file. |
Ledger kinds: call, number_rental, plan_fee, tax, setup_assistant, refund, topup, credit, adjustment, opening_balance.
Request
curl "https://api.sevennadi.si/billing/ledger?kind=call,topup&from=2026-10-01&page_size=2" \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"page_number": 1,
"page_size": 2,
"total": 1,
"has_more": false,
"data": [
{
"id": 63,
"kind": "credit",
"amount": 5.0,
"balance_after": 5.0,
"currency": "INR",
"description": "Sign-up credit",
"reference": "signup:03d064aa-fceb-4c80-b915-2b6090c7021f",
"execution_id": null,
"payment_id": null,
"invoice_id": null,
"minutes": 0.0,
"included_minutes": 0.0,
"created_at": "2026-10-13T07:21:47Z"
}
]
}amount is in major units: negative for a debit, positive for a credit. reference is unique per entry, which is what makes charging idempotent (a call is call:<execution id>, a top-up is topup:<payment id>). included_minutes is the part of a call covered by the plan's included minutes.
The CSV has the columns id, created_at, kind, amount, balance_after, currency, description, reference, execution_id, minutes, included_minutes.
Errors
| Status | Message |
|---|---|
| 400 | page_number must be >= 1 and page_size between 1 and 200 |
| 400 | kind must be one of: call, number_rental, plan_fee, tax, setup_assistant, refund, topup, credit, adjustment, opening_balance |
| 400 | from must be an ISO date or date-time (also for to) |
| 400 | format must be json or csv |
| 404 | Execution not found (execution_id is not a UUID) |
Get usage per day
GET /billing/usage
Returns spend per day, built from the ledger's debits, and totals by ledger kind. Days are in the operator's billing time zone and include today.
Auth: API key or session token. Role: admin and up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
days | query | integer | no | 1 to 366. Default 30. |
Request
curl "https://api.sevennadi.si/billing/usage?days=2" \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"currency": "INR",
"days": [
{"date": "2026-10-12", "spend": 0.0, "minutes": 0.0, "calls": 0},
{"date": "2026-10-13", "spend": 0.42, "minutes": 3.5, "calls": 2}
],
"by_kind": {"call": 0.42},
"total_spend": 0.42,
"total_minutes": 3.5,
"total_calls": 2
}spend is in major units. minutes and calls count ledger entries of kind call only.
Errors
| Status | Message |
|---|---|
| 400 | days must be between 1 and 366 |
Prices and plans
Get the price book
GET /billing/price-book
Returns the per-minute rates that apply to this workspace: the platform's price book with any overrides the operator set for the workspace. A call costs its minutes multiplied by the sum of the platform fee, the telephony rate for its provider and direction, plus the rate of each speech-to-text, LLM and text-to-speech model it used.
Auth: API key or session token. Role: viewer and up.
Request
curl https://api.sevennadi.si/billing/price-book \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK (shortened)
{
"currency": "INR",
"unit": "minor units per minute",
"custom": false,
"platform": {"per_minute": 2.0},
"telephony": {
"default": {"outbound": 1.0, "inbound": 1.0},
"web": {"outbound": 0.0, "inbound": 0.0}
},
"components": {
"transcriber": {"default": 0.9, "local": {"default": 0.0}},
"llm": {"default": 0.9, "local": {"default": 0.0}},
"synthesizer": {"default": 3.0, "local": {"default": 0.0}}
}
}Rates are minor units per minute: 2.0 in an INR book is 0.02 rupees a minute. Under components, each kind has a default rate and one object per provider with a default and optional per-model rates. custom is true when the workspace has its own overrides.
Limitation: The numbers shown are the placeholder rates shipped with the platform. They are not real prices; the operator sets them.
List plans
GET /billing/plans
Lists the plans in the price book and the one the workspace is on.
Auth: API key or session token. Role: viewer and up.
Request
curl https://api.sevennadi.si/billing/plans \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"currency": "INR",
"current": "trial",
"status": "active",
"plans": [
{
"id": "payg",
"name": "Starter",
"kind": "payg",
"description": "No monthly fee. Calls are charged per minute from the wallet.",
"monthly_fee": 0.0,
"included_minutes": 0.0,
"concurrency_limit": 10,
"self_serve": true
}
]
}monthly_fee is in major units. self_serve: false means only the operator can assign the plan. The plans themselves are operator-configured examples.
Choose a plan
POST /billing/plan
Moves the workspace to a self-serve plan and applies the plan's concurrency limit. A monthly plan's fee for the current month is debited from the wallet at once. Without enough balance the plan is past_due (no included minutes) until a top-up lets the fee through.
Auth: session token. Role: owner.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
plan_id | body | string | yes | A plan id with self_serve: true. |
Idempotency-Key | header | string | no | Makes a retry safe: the same key with the same request returns the first response. See Idempotency. |
Request
curl -X POST https://api.sevennadi.si/billing/plan \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"plan_id": "payg"}'Response 200 OK
{
"current": "payg",
"status": "active",
"plan": {
"id": "payg",
"name": "Starter",
"kind": "payg",
"description": "No monthly fee. Calls are charged per minute from the wallet.",
"monthly_fee": 0.0,
"included_minutes": 0.0,
"concurrency_limit": 10,
"self_serve": true
}
}Errors
| Status | Message |
|---|---|
| 400 | Unknown plan 'gold' |
| 400 | This plan is assigned by our team |
| 400 | A sub-account is on its parent's plan |
| 403 | Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace. |
Invoices
Invoices are issued by a daily background job for the month that ended, for workspaces with billable usage. Lines are grouped from the ledger and add up to what the wallet was charged.
List invoices
GET /billing/invoices
Lists the workspace's invoices, newest period first.
Auth: API key or session token. Role: admin and up.
Request
curl https://api.sevennadi.si/billing/invoices \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"invoices": [
{
"id": "9a3e7a51-0c1b-4b1b-8f3e-5f6b9d2c7a10",
"number": "INV/2627/00001",
"period": "2026-09",
"period_start": "2026-08-31T18:30:00Z",
"period_end": "2026-09-30T18:30:00Z",
"currency": "INR",
"subtotal": 120.0,
"tax_total": 21.6,
"total": 141.6,
"tax": {"mode": "exclusive", "rate_percent": 18.0, "cgst": 10.8, "sgst": 10.8, "igst": 0.0, "place_of_supply": "06", "hsn_sac": "998319"},
"line_items": [
{"description": "Platform fee", "kind": "call", "quantity": 400.0, "unit": "minutes", "amount": 8.0, "hsn_sac": "998319"},
{"description": "Phone number rental", "kind": "number_rental", "quantity": 1.0, "unit": "numbers", "amount": 112.0, "hsn_sac": "998319"}
],
"seller": {"name": "7Nadi", "tax_id": null, "address": null, "state": "Haryana", "state_code": "06"},
"buyer": {"name": "Example Bank Ltd", "tax_id": "06ABCDE1234F1Z5", "email": "accounts@example.test", "address": "12 Example Road, Gurugram, Haryana, 122001", "state": "Haryana", "state_code": "06", "country": "IN"},
"status": "issued",
"created_at": "2026-10-01T20:10:05Z",
"pdf_url": "/billing/invoices/9a3e7a51-0c1b-4b1b-8f3e-5f6b9d2c7a10/pdf"
}
]
}Money fields are in major units. status is issued or void. tax, line_items, seller and buyer are a snapshot taken when the invoice was issued; tax also carries the per-component rate fields. The invoice number is <prefix>/<financial year>/<5 digits> with the operator's prefix. The amounts in the example are illustrative.
Get an invoice
GET /billing/invoices/{invoice_id}
Returns one invoice in the same shape as the list.
Auth: API key or session token. Role: admin and up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
invoice_id | path | string (UUID) | yes | The invoice's id. |
Request
curl https://api.sevennadi.si/billing/invoices/9a3e7a51-0c1b-4b1b-8f3e-5f6b9d2c7a10 \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
One invoice object, as in List invoices.
Errors
| Status | Message |
|---|---|
| 404 | Invoice not found |
Download an invoice PDF
GET /billing/invoices/{invoice_id}/pdf
Returns the invoice as a PDF file (Content-Type: application/pdf, sent as an attachment named after the invoice number).
Auth: API key or session token. Role: admin and up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
invoice_id | path | string (UUID) | yes | The invoice's id. |
Request
curl -o invoice.pdf https://api.sevennadi.si/billing/invoices/9a3e7a51-0c1b-4b1b-8f3e-5f6b9d2c7a10/pdf \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK with the PDF bytes.
Limitation: The PDF uses built-in Latin-1 fonts. Characters from other scripts in a company name print as
?.
Errors
| Status | Message |
|---|---|
| 404 | Invoice not found |
Top-ups
Start a top-up
POST /billing/topups
Creates a payment order for amount and returns a checkout_url to send the customer to. The wallet is credited only when the provider's signed webhook confirms the payment. After a credit, batches and campaigns that paused for low balance resume.
With the mock provider there is no checkout page: the payment settles at once and the response already has status: "paid".
Auth: session token. Role: owner. A sub-account cannot top up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
amount | body | number | yes | Major units, between the payments.min and payments.max of the balance. |
return_url | body | string | no | Where the provider sends the customer afterwards. Must be a page of the operator's dashboard. Default: the dashboard's billing page. |
Idempotency-Key | header | string | no | Makes a retry safe: the same key with the same request returns the first response. See Idempotency. |
Request
curl -X POST https://api.sevennadi.si/billing/topups \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"amount": 500}'Response 201 Created
{
"id": "2f6d5d0c-74f6-4a0e-9d21-0c9b1c7c3e55",
"provider": "razorpay",
"amount": 500.0,
"currency": "INR",
"status": "created",
"checkout_url": "https://payments.example.test/checkout/abc123",
"refunded_amount": 0.0,
"error": null,
"created_at": "2026-10-13T09:00:00Z",
"paid_at": null,
"balance": 5.0
}status is created, paid, failed or refunded. checkout_url is present only while the status is created. balance is the wallet after the request.
Errors
| Status | Message |
|---|---|
| 400 | amount must be a number |
| 400 | amount must be between INR 1.00 and INR 500,000.00 (the operator's limits) |
| 400 | return_url must be a page of the dashboard |
| 400 | A sub-account spends its parent's wallet: add funds on the parent account |
| 403 | Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace. |
| 502 | The provider refused or could not be reached, for example Razorpay answered HTTP 500 or Stripe is unreachable (ConnectTimeout). |
| 503 | The payment provider is not usable, for example The mock payment provider is off in production: set PAYMENT_PROVIDER and its credentials. |
List top-ups
GET /billing/topups
Lists the workspace's last 100 payments, newest first.
Auth: API key or session token. Role: admin and up.
Request
curl https://api.sevennadi.si/billing/topups \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"payments": [
{
"id": "2f6d5d0c-74f6-4a0e-9d21-0c9b1c7c3e55",
"provider": "mock",
"amount": 500.0,
"currency": "INR",
"status": "paid",
"checkout_url": null,
"refunded_amount": 0.0,
"error": null,
"created_at": "2026-10-13T09:00:00Z",
"paid_at": "2026-10-13T09:00:00Z"
}
]
}Get a top-up
GET /billing/topups/{payment_id}
Returns one payment. Poll it after the customer returns from the checkout page to see whether the webhook has arrived.
Auth: API key or session token. Role: admin and up.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
payment_id | path | string (UUID) | yes | The payment's id. |
Request
curl https://api.sevennadi.si/billing/topups/2f6d5d0c-74f6-4a0e-9d21-0c9b1c7c3e55 \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
One payment object, as in List top-ups.
Errors
| Status | Message |
|---|---|
| 404 | Payment not found |
Receive a payment webhook
POST /billing/webhooks/{provider}
Called by the payment provider, not by you. Nothing in the body is believed unless the provider's signature verifies. Delivering the same event twice is harmless.
Auth: none. The request is trusted only by its signature: X-Razorpay-Signature for razorpay, Stripe-Signature for stripe, and an HMAC made with the platform's own secret for mock.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes | mock, razorpay or stripe. The provider must be configured. |
| (body) | body | bytes | yes | The provider's event, at most 256,000 bytes. |
Request
curl -X POST https://api.sevennadi.si/billing/webhooks/razorpay \
-H "Content-Type: application/json" \
-H "X-Razorpay-Signature: SIGNATURE_FROM_THE_PROVIDER" \
-d '{"event": "payment_link.paid"}'Response 200 OK
{"received": true, "result": "credited"}result | Meaning |
|---|---|
credited | The payment was marked paid and the wallet credited. |
duplicate | The payment was already paid or refunded; nothing changed. |
failed | The provider reported a failed payment; the payment is marked failed. |
ignored | An event type the platform does not act on. |
unknown | No payment matches the event. |
Errors
| Status | Message |
|---|---|
| 400 | Webhook rejected (bad signature or malformed event, or the paid amount or currency does not match the order; nothing is credited) |
| 404 | Not Found (unknown or unconfigured provider) |
| 413 | Payload too large |
Limitation: The Razorpay and Stripe webhook handling was written from the providers' public documentation and tested against local fakes only. Run each provider's test mode end to end before taking money.
Tax details
Get tax details
GET /billing/tax-details
Returns who the workspace's invoices are made out to. Fields that were never set are null.
Auth: API key or session token. Role: admin and up.
Request
curl https://api.sevennadi.si/billing/tax-details \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{
"legal_name": "Example Bank Ltd",
"tax_id": "06ABCDE1234F1Z5",
"billing_email": "accounts@example.test",
"address_line1": "12 Example Road",
"address_line2": null,
"city": "Gurugram",
"state": "Haryana",
"state_code": "06",
"postal_code": "122001",
"country": "IN"
}Set tax details
PUT /billing/tax-details
Updates the fields you send and leaves the others as they are. Send null or an empty string to clear a field. Unknown fields are refused.
Auth: session token. Role: owner.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
legal_name | body | string | no | Up to 300 characters. |
tax_id | body | string | no | Up to 40 characters; stored upper-case without spaces. Checked for GSTIN shape when the country is IN or unset. |
billing_email | body | string | no | Up to 320 characters; must be an email address. |
address_line1 | body | string | no | Up to 300 characters. |
address_line2 | body | string | no | Up to 300 characters. |
city | body | string | no | Up to 120 characters. |
state | body | string | no | Up to 120 characters. |
state_code | body | string | no | The 2-digit GST state code. Decides CGST+SGST or IGST on invoices. |
postal_code | body | string | no | Up to 20 characters. |
country | body | string | no | 2-letter country code; stored upper-case. |
Request
curl -X PUT https://api.sevennadi.si/billing/tax-details \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"legal_name": "Example Bank Ltd", "tax_id": "06ABCDE1234F1Z5", "state_code": "06", "country": "IN"}'Response 200 OK
The full tax details, as in Get tax details.
Errors
| Status | Message |
|---|---|
| 400 | Unknown field vat |
| 400 | legal_name must be text |
| 400 | legal_name: at most 300 characters |
| 400 | tax_id is not a valid GSTIN (15 characters, e.g. 06ABCDE1234F1Z5) |
| 400 | state_code must be the 2-digit GST state code, e.g. 06 |
| 400 | billing_email must be an email address |
| 403 | Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace. |
Alerts
The workspace's owners get one email when the balance drops under the threshold and one when it runs out, and not again until a top-up. Emails go through the operator's email backend (the default stores them in an outbox table and sends nothing).
Get alert settings
GET /billing/alerts
Returns the low-balance threshold in use.
Auth: API key or session token. Role: admin and up.
Request
curl https://api.sevennadi.si/billing/alerts \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"Response 200 OK
{"low_balance_threshold": 1.0, "custom": false, "currency": "INR"}custom is false when the workspace uses the operator's default threshold.
Set alert settings
PUT /billing/alerts
Sets the workspace's own low-balance threshold.
Auth: session token. Role: owner.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
low_balance_threshold | body | number or null | yes | Major units, 0 or more. 0 turns the low-balance email off (the "balance has run out" email is still sent). null goes back to the operator's default. |
Request
curl -X PUT https://api.sevennadi.si/billing/alerts \
-H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"low_balance_threshold": 100}'Response 200 OK
{"low_balance_threshold": 100.0, "custom": true, "currency": "INR"}Errors
| Status | Message |
|---|---|
| 400 | low_balance_threshold must be a number >= 0, or null |
| 403 | Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace. |