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.

OperationMethod and pathMinimum role
Get the balanceGET /billing/balanceviewer
List ledger entriesGET /billing/ledgeradmin
Get usage per dayGET /billing/usageadmin
Get the price bookGET /billing/price-bookviewer
List plansGET /billing/plansviewer
Choose a planPOST /billing/planowner
List invoicesGET /billing/invoicesadmin
Get an invoiceGET /billing/invoices/{invoice_id}admin
Download an invoice PDFGET /billing/invoices/{invoice_id}/pdfadmin
Start a top-upPOST /billing/topupsowner
List top-upsGET /billing/topupsadmin
Get a top-upGET /billing/topups/{payment_id}admin
Receive a payment webhookPOST /billing/webhooks/{provider}none
Get tax detailsGET /billing/tax-detailsadmin
Set tax detailsPUT /billing/tax-detailsowner
Get alert settingsGET /billing/alertsadmin
Set alert settingsPUT /billing/alertsowner

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 unit field.
  • 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:

text
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

bash
curl https://api.sevennadi.si/billing/balance \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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"}
}
FieldMeaning
balance, low_balance_thresholdMajor units. low is true when the balance is under the threshold.
plan.kindtrial, payg or monthly.
plan.statusactive, or past_due when a monthly fee could not be debited.
plan.periodThe billing month, YYYY-MM, in the operator's billing time zone.
payments.availableWhether the configured provider has its credentials (for mock: whether this is not production).
payments.min, payments.maxSmallest and largest single top-up, major units.
tax.modeexclusive (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.

ParameterInTypeRequiredDescription
page_numberqueryintegernoFrom 1. Default 1.
page_sizequeryintegerno1 to 200. Default 50.
kindquerystringnoOne or more kinds, comma-separated. See below.
execution_idquerystring (UUID)noOnly the entries of one call.
fromquerystringnoISO date or date-time; entries created at or after it.
toquerystringnoISO date or date-time; entries created at or before it.
formatquerystringnojson (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

bash
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

json
{
  "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

StatusMessage
400page_number must be >= 1 and page_size between 1 and 200
400kind must be one of: call, number_rental, plan_fee, tax, setup_assistant, refund, topup, credit, adjustment, opening_balance
400from must be an ISO date or date-time (also for to)
400format must be json or csv
404Execution 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.

ParameterInTypeRequiredDescription
daysqueryintegerno1 to 366. Default 30.

Request

bash
curl "https://api.sevennadi.si/billing/usage?days=2" \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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

StatusMessage
400days 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

bash
curl https://api.sevennadi.si/billing/price-book \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK (shortened)

json
{
  "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

bash
curl https://api.sevennadi.si/billing/plans \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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.

ParameterInTypeRequiredDescription
plan_idbodystringyesA plan id with self_serve: true.
Idempotency-KeyheaderstringnoMakes a retry safe: the same key with the same request returns the first response. See Idempotency.

Request

bash
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

json
{
  "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

StatusMessage
400Unknown plan 'gold'
400This plan is assigned by our team
400A sub-account is on its parent's plan
403Your 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

bash
curl https://api.sevennadi.si/billing/invoices \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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.

ParameterInTypeRequiredDescription
invoice_idpathstring (UUID)yesThe invoice's id.

Request

bash
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

StatusMessage
404Invoice 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.

ParameterInTypeRequiredDescription
invoice_idpathstring (UUID)yesThe invoice's id.

Request

bash
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

StatusMessage
404Invoice 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.

ParameterInTypeRequiredDescription
amountbodynumberyesMajor units, between the payments.min and payments.max of the balance.
return_urlbodystringnoWhere the provider sends the customer afterwards. Must be a page of the operator's dashboard. Default: the dashboard's billing page.
Idempotency-KeyheaderstringnoMakes a retry safe: the same key with the same request returns the first response. See Idempotency.

Request

bash
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

json
{
  "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

StatusMessage
400amount must be a number
400amount must be between INR 1.00 and INR 500,000.00 (the operator's limits)
400return_url must be a page of the dashboard
400A sub-account spends its parent's wallet: add funds on the parent account
403Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace.
502The provider refused or could not be reached, for example Razorpay answered HTTP 500 or Stripe is unreachable (ConnectTimeout).
503The 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

bash
curl https://api.sevennadi.si/billing/topups \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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.

ParameterInTypeRequiredDescription
payment_idpathstring (UUID)yesThe payment's id.

Request

bash
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

StatusMessage
404Payment 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.

ParameterInTypeRequiredDescription
providerpathstringyesmock, razorpay or stripe. The provider must be configured.
(body)bodybytesyesThe provider's event, at most 256,000 bytes.

Request

bash
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

json
{"received": true, "result": "credited"}
resultMeaning
creditedThe payment was marked paid and the wallet credited.
duplicateThe payment was already paid or refunded; nothing changed.
failedThe provider reported a failed payment; the payment is marked failed.
ignoredAn event type the platform does not act on.
unknownNo payment matches the event.

Errors

StatusMessage
400Webhook rejected (bad signature or malformed event, or the paid amount or currency does not match the order; nothing is credited)
404Not Found (unknown or unconfigured provider)
413Payload 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

bash
curl https://api.sevennadi.si/billing/tax-details \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "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.

ParameterInTypeRequiredDescription
legal_namebodystringnoUp to 300 characters.
tax_idbodystringnoUp to 40 characters; stored upper-case without spaces. Checked for GSTIN shape when the country is IN or unset.
billing_emailbodystringnoUp to 320 characters; must be an email address.
address_line1bodystringnoUp to 300 characters.
address_line2bodystringnoUp to 300 characters.
citybodystringnoUp to 120 characters.
statebodystringnoUp to 120 characters.
state_codebodystringnoThe 2-digit GST state code. Decides CGST+SGST or IGST on invoices.
postal_codebodystringnoUp to 20 characters.
countrybodystringno2-letter country code; stored upper-case.

Request

bash
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

StatusMessage
400Unknown field vat
400legal_name must be text
400legal_name: at most 300 characters
400tax_id is not a valid GSTIN (15 characters, e.g. 06ABCDE1234F1Z5)
400state_code must be the 2-digit GST state code, e.g. 06
400billing_email must be an email address
403Your 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

bash
curl https://api.sevennadi.si/billing/alerts \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{"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.

ParameterInTypeRequiredDescription
low_balance_thresholdbodynumber or nullyesMajor 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

bash
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

json
{"low_balance_threshold": 100.0, "custom": true, "currency": "INR"}

Errors

StatusMessage
400low_balance_threshold must be a number >= 0, or null
403Your role (admin) can't change billing. It needs owner access; ask an owner of the workspace.