Sub-accounts
A sub-account is a workspace your organization creates for one of your own customers — and pays for. Build your own product on Telenow (your login, your admin, your payment gateways) and create one sub-account per customer: each gets its own agents, numbers, calls, knowledge bases, webhooks and its own API key, fully separated from every other customer's, while every charge comes out of your wallet.
| Your organization (the parent) | Each sub-account | |
|---|---|---|
| Wallet & credits | ✅ Yours — top up here | ❌ None of its own — every charge is posted to the parent's wallet |
| Pricing | Your rates and negotiated platform fee | Exactly the parent's |
| Invoice / Billing page | Shows the parent's spend and every sub-account's | Shows only what that sub-account spent |
| API key | Your keys | Its own key, returned when you create it |
| Limits | Your own usage limits | Set by the parent: live calls, monthly spend, total spend, suspended |
| Webhooks | Receives its own events and every sub-account's | Receives its own |
Creating a sub-account is quiet: it is not a sign-up, so its users get no Telenow emails, OTPs or welcome credit, and nobody from Telenow contacts them.
Who can use these endpoints: the parent's owner or admin — with an
owner/adminAPI key, or signed in. Adeveloperorviewerkey gets403: a sub-account spends your money and carries a credential. A sub-account's own key gets403too — a sub-account cannot have sub-accounts.
Endpoints
The same operations exist on both surfaces. Use the Integration API with your API key from your backend; the dashboard routes are what the Workplace → Sub-accounts page uses.
| Method | Integration API (X-API-Key, flat JSON) | Dashboard API ({ success, data }) | Purpose |
|---|---|---|---|
GET | /api/v1/sub-accounts | /api/orgs/{orgId}/sub-accounts | List, newest first |
POST | /api/v1/sub-accounts | /api/orgs/{orgId}/sub-accounts | Create — returns the sub-account and its API key |
GET | /api/v1/sub-accounts/{id} | /api/orgs/{orgId}/sub-accounts/{id} | One sub-account |
PATCH | /api/v1/sub-accounts/{id} | /api/orgs/{orgId}/sub-accounts/{id} | Rename, set your reference, change limits |
POST | /api/v1/sub-accounts/{id}/suspend | …/{id}/suspend | Refuse new calls; end live ones |
POST | /api/v1/sub-accounts/{id}/resume | …/{id}/resume | Lift a suspension |
GET | /api/v1/sub-accounts/{id}/usage | …/{id}/usage | Calls, minutes and spend for a window |
POST | /api/v1/sub-accounts/{id}/keys | …/{id}/keys | Mint a new key (revokes the others by default) |
DELETE | /api/v1/sub-accounts/{id} | …/{id} | Close for good |
A sub-account id is its organization id. Another organization's id returns 404.
Create a sub-account
curl -X POST https://api.telenow.ai/api/v1/sub-accounts \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{
"name": "Acme Dental",
"externalId": "cust_8412",
"limits": {
"maxConcurrentCalls": 5,
"monthlySpendLimitUsd": 200,
"totalSpendLimitUsd": null
}
}'
Returns 201 Created:
{
"subAccount": {
"id": "6f1c…",
"name": "Acme Dental",
"externalId": "cust_8412",
"status": "active",
"limits": { "maxConcurrentCalls": 5, "monthlySpendLimitUsd": 200.0, "totalSpendLimitUsd": null },
"suspendedAt": null,
"suspendedReason": null,
"closedAt": null,
"createdAt": "2026-10-16T09:12:44.120Z",
"updatedAt": "2026-10-16T09:12:44.120Z",
"usage": { "monthToDateSpendUsd": 0.0, "activeCalls": 0 }
},
"apiKey": {
"id": "a41e…",
"key": "vai_live_…",
"lastFour": "Xk2Q",
"name": "Sub-account key",
"role": "admin",
"createdAt": "2026-10-16T09:12:44.120Z"
}
}
apiKey.key is shown exactly once. Store it encrypted on your server; it is the sub-account's credential for every other Telenow API — agents, numbers, calls, campaigns, webhooks, knowledge bases — exactly as your own key is for you. Never send it to a browser: mint a client token with it instead.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Required. Up to 160 characters. |
externalId | string | none | Your id for this customer, unique among your sub-accounts (409 if reused). Up to 200 characters. Find a sub-account by it with ?externalId=. |
limits | object | no limits | See Limits. |
keyName | string | Sub-account key | Name of the key minted with it. |
keyRole | string | admin | admin, developer or viewer. Never owner. |
Unknown fields are rejected with 400 rather than ignored — a misspelt limit would otherwise create a sub-account with no limits.
The sub-account copies your country, currency, timezone, consent enforcement, compliance mode and data-residency region when it is created.
Limits
| Field | Meaning |
|---|---|
maxConcurrentCalls | Live calls at once — inbound, outbound, web and campaign calls together. |
monthlySpendLimitUsd | USD this sub-account may spend in the current billing month (the same calendar month, India time, as your own cap and invoice). |
totalSpendLimitUsd | USD this sub-account may spend, ever. |
Each one: absent = unchanged (on create: no limit), null = no limit, a number = that limit, 0 = stopped. Spend is what the sub-account's calls and purchases were charged, the same figure that left your wallet.
When a limit is reached, the sub-account's new calls are refused the way an out-of-credit account's are: an outbound or API call gets 403 (or 429 for the live-call limit), a campaign waits and retries, and an inbound call is turned away busy before it is answered on Twilio, Plivo, SIP trunks and WhatsApp calling. On Vobiz and Vonage a refused inbound call follows the number's missed-call setting (forward or message), as it does for any account; on Exotel and Smartflo the call is dropped when its audio stream opens. Calls already live when a spend limit or a suspension takes effect are ended within the next spend-guard tick (a suspension through this API ends them at once).
A sub-account is also refused when your account would be: your wallet out of credits (prepaid), your account suspended, or your monthly spend cap reached — your cap counts every sub-account's spend. What a sub-account is told in those cases never includes your balance, cap or spend.
Update, suspend, resume
curl -X PATCH https://api.telenow.ai/api/v1/sub-accounts/6f1c… \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "limits": { "monthlySpendLimitUsd": 500, "maxConcurrentCalls": null } }'
PATCH takes name, externalId (null clears it) and limits; an empty body is 400. A closed sub-account can't be changed (409).
curl -X POST https://api.telenow.ai/api/v1/sub-accounts/6f1c…/suspend \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "reason": "Invoice overdue" }'
Suspending refuses every new call at once and ends the sub-account's live calls. The optional reason (up to 500 characters) is appended to the refusal its calls get. …/resume lifts it. A typical product suspends a customer's sub-account when that customer's own balance with you runs out, and resumes it when they top up.
Usage
curl "https://api.telenow.ai/api/v1/sub-accounts/6f1c…/usage?from=2026-10-01T00:00:00Z" \
-H "X-API-Key: vai_live_…"
{
"subAccountId": "6f1c…",
"from": "2026-10-01T00:00:00Z",
"to": "2026-10-16T09:20:00Z",
"calls": 412,
"billableSeconds": 51840,
"spendUsd": 63.12,
"activeCalls": 2,
"monthToDateSpendUsd": 63.12,
"totalSpendUsd": 180.4,
"limits": { "maxConcurrentCalls": 5, "monthlySpendLimitUsd": 200.0, "totalSpendLimitUsd": null },
"status": "active"
}
from and to are RFC 3339 instants; they default to the start of the current billing month and now. calls and billableSeconds count calls that started in the window; spendUsd is what was charged in it — a call is charged when it settles, shortly after it ends. Use these figures to bill your customer.
For per-call detail, call GET /api/v1/calls with the sub-account's key, or subscribe your own endpoint to call.ended — see Webhooks.
List
GET /api/v1/sub-accounts — query: status (active, suspended, closed), externalId (exact), limit (default 50, max 200), offset.
{ "subAccounts": [ { "id": "6f1c…", "name": "Acme Dental", "status": "active", "usage": { "monthToDateSpendUsd": 63.12, "activeCalls": 2 }, "…": "…" } ], "total": 1, "limit": 50, "offset": 0 }
Keys
curl -X POST https://api.telenow.ai/api/v1/sub-accounts/6f1c…/keys \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "keyRole": "admin", "revokeExisting": true }'
Mints a new key (returned once, as on create) and — unless you send "revokeExisting": false — revokes every other key of that sub-account in the same step. Use it to rotate, or if a key leaked. Returns { "subAccountId", "revokedKeys", "apiKey" } with 201.
A sub-account's keys act as your organization's owner inside that sub-account. If that person leaves, rotate each sub-account's key to re-attach it to the current owner.
Close
DELETE /api/v1/sub-accounts/{id} closes it for good: its keys are revoked, new calls are refused, live ones are ended, running campaigns are paused and the workspace is deleted. It is refused with 409 while the sub-account still holds phone numbers — their rent is charged to you, so release them first (with the sub-account's key). Closing twice is harmless.
Webhooks
Every event a sub-account raises carries subAccountId (its id) and is delivered to:
- the sub-account's own endpoints, as usual, and
- every org-wide endpoint of the parent (one with
agentId: null) whoseeventsmatch.
So one endpoint on your own organization receives every customer's calls; route them by subAccountId. Your agent-scoped endpoints never receive a sub-account's events. The parent's copy appears in your delivery log. See Webhook events.
Errors
| Status | When |
|---|---|
400 | Invalid field, unknown field, empty update, bad timestamp. |
403 | Not an owner/admin (person or key role); a sub-account's key; a demo workspace. |
404 | Not one of your sub-accounts. |
409 | externalId already used; changing or closing a closed one; closing while it holds numbers. |