Authentication
Authentication
Telenow accepts three credentials. Which one you use is decided by where the code runs, not by preference:
| Credential | Header | Runs on | Identifies |
|---|---|---|---|
| API key | X-API-Key: vai_live_… | Your server | An organization + a role |
| User JWT | Authorization: Bearer eyJ… (+ X-Org-Id) | Dashboard, your own admin UI | A signed‑in user |
| Client token | Authorization: Bearer … | A browser or mobile app | One pre‑authorized call |
Never put an API key in a browser, mobile app, or anything a user can view source on. A key is a bearer credential for your whole organization. For client‑side voice, mint a client token on your server and send that instead — it expires in minutes and can only start the one call you scoped it to.
Where each credential works
This table is the authoritative answer. If a code sample anywhere in these docs disagrees with it, the table is right — please tell us.
| Surface | API key | User JWT | Notes |
|---|---|---|---|
/api/v1/* — Integration API | ✅ | ❌ | Key only, by design. Flat JSON, slim projections. |
/api/v1/integrations/* — Integration connectors | ✅ | ❌ | Connect a store, CRM, database or WhatsApp number by key. Secrets are write‑only: accepted here, returned only as a mask. |
/api/agents/* | ✅ | ✅ | Copilot + simulation sub‑routes are dashboard‑only (JWT). |
/api/orgs/{orgId}/ — calls, recordings, call-audio, webhooks, knowledge-bases, ambient-tracks only | ✅ | ✅ | A key is pinned to its own org; another org's {orgId} returns 403. |
/api/orgs/{orgId}/ — everything else (analytics, usage, billing, audit, dnc, campaigns, memory, apps, templates, carriers, trunks, mcp, whatsapp*, integrations, …) | ❌ | ✅ | Dashboard‑only so far. Tell us if you need one of these programmatically. Connectors are the exception — manage them by key at /api/v1/integrations/* above. This dashboard route stays JWT‑only because it also carries the action bench, which fires real vendor requests. |
/api/orgs/{orgId} itself — org profile, members | ❌ | ✅ | |
/api/sessions/ — initiate-call, init-web-call, {id}/transfer, DELETE {id} | ✅ | ✅ | Start, transfer and end calls server‑to‑server. |
/api/sessions/{id}, {id}/messages, {id}/tool-invocations (reads) | ❌ | ✅ | Read transcripts with GET /api/v1/calls/{id} instead. |
/api/client-tokens | ✅ | ✅ | Mints browser tokens. Never call this from a browser. |
/api/catalog, /api/providers/*, /api/voice/numbers/* | ✅ | ✅ | |
/api/auth/* | ❌ | ✅ | Login, password, MFA — a session concern, not a machine one. |
/api/orgs/{orgId}/api-keys | ❌ | ✅ | A key cannot mint or revoke keys. Prevents a leaked key minting itself a replacement that survives revocation. |
/api/app-* — App Platform | ❌ | ❌ | Separate scheme: app keys. See App scopes. |
Read vs. write with a key
On the routes above that were historically dashboard‑only, a key's role decides whether it may mutate:
owner,admin,developer— full read + write.member,viewer— read‑only. AnyPOST/PUT/PATCH/DELETEreturns403with"This API key's role ('viewer') is read-only on this endpoint."
This does not apply to POST /api/v1/chat or POST /api/sessions/initiate-call, which any valid key may call — they're the normal way low‑privilege integrations do their job.
The same bar applies on /api/v1 itself, worded slightly differently: "This API key's role (viewer) is read-only; owner, admin or developer is required for write operations". On connectors that covers testing as well as writing — a verification spends a real request against the vendor using your stored credential, which a read‑only key is not entitled to spend.
API keys (recommended for integrations)
An API key is scoped to one organization and carries a role. Create one in the dashboard, or via the API.
Create a key in the dashboard
- In the sidebar, go to Developers → API keys.
- Click New key (top right). You need the owner, admin, or developer role — viewers can see keys but can't mint them.
- Give the key a Name (e.g.
Production backend) and click Create. - The full secret is shown once in a Save your API key dialog. Copy it now and store it somewhere secure — Telenow keeps only a hash and will never show it again.
- Click I've saved it. The key now appears in the table with its last‑four digits, creation date, last‑used time, and status.
To revoke a key, click the trash icon on its row (owner/admin only). Revocation is immediate — the next request with that key fails with 401.
Using a key
Send it in the X-API-Key header. Use the full secret (not a Bearer prefix). Because the key already identifies the organization, you do not send an X-Org-Id header:
curl https://api.telenow.ai/api/agents \
-H "X-API-Key: vai_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
The same header works on both surfaces:
# Integration API (X-API-Key only, flat JSON)
curl https://api.telenow.ai/api/v1/me \
-H "X-API-Key: vai_live_…"
Key facts:
- Secrets are prefixed
vai_live_followed by a random token. The full secret is shown once at creation — store it securely; Telenow keeps only a SHA‑256 hash. - Keys can be revoked at any time, immediately invalidating them.
- A key's role (
owner/admin/developer/viewer) governs what it can do (see Roles). New keys default todeveloper. - If the user who created a key is later deactivated or deleted, the key stops working (
401 — "API key creator is no longer active"). - The key's last‑used timestamp updates automatically on every successful request.
Keep keys server‑side. Never embed an API key in a browser, mobile app, or public widget. For browser experiences use the public widget or mint short‑lived sessions from your backend (see Programmatic web calls).
Key roles
A key inherits a role at creation; that role is checked on every request.
| Role | Typical use | Can create keys? | Can revoke keys? | Can manage webhooks/hooks? |
|---|---|---|---|---|
owner | Workspace owner | Yes | Yes | Yes |
admin | Administrator | Yes | Yes | Yes |
developer | Backend/CI service account | Yes | No | Yes |
viewer | Read‑only / reporting | No | No | No (read‑only) |
On the Integration API, hook management (POST/DELETE /api/v1/hooks) requires owner, admin, or developer; viewer keys are read‑only there. See Organizations, team & API keys for the full role model and Webhooks.
Managing keys via API
These endpoints are part of the Dashboard API and require a user JWT (an API key cannot mint or revoke keys for its own org through this route):
GET /api/orgs/{orgId}/api-keys # list
POST /api/orgs/{orgId}/api-keys # create -> returns secret once
DELETE /api/orgs/{orgId}/api-keys/{keyId} # revoke
Create request:
curl -X POST https://api.telenow.ai/api/orgs/{orgId}/api-keys \
-H "Authorization: Bearer eyJ…" \
-H "Content-Type: application/json" \
-d '{ "name": "CI integration", "role": "developer" }'
role is optional and defaults to developer. Create response (secret returned exactly once):
{
"success": true,
"data": {
"key": {
"id": "…", "org_id": "…", "name": "CI integration", "role": "developer",
"last_four": "abcd", "created_by": "…", "last_used_at": null,
"revoked_at": null, "created_at": "…"
},
"secret": "vai_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
The list endpoint returns { "success": true, "data": { "apiKeys": [ … ], "total": N } }; each row shows last_four rather than the secret. See Organizations, team & API keys.
User JWT (bearer token)
A JWT represents a signed‑in user. Obtain one by logging in:
curl -X POST https://api.telenow.ai/api/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "password": "••••••••" }'
Response:
{
"success": true,
"data": {
"user": { "id": "…", "email": "[email protected]", "role": "owner" },
"tokens": { "accessToken": "eyJ…", "refreshToken": "eyJ…" }
}
}
Send the access token on subsequent requests, plus the organization id for org‑scoped routes:
curl https://api.telenow.ai/api/orgs/{orgId}/calls \
-H "Authorization: Bearer eyJ…" \
-H "X-Org-Id: {orgId}"
- Access tokens expire; use
POST /api/auth/refreshwith the refresh token ({ "refreshToken": "eyJ…" }) to get a fresh pair. - An expired token returns
401withcode: "token_expired"; an otherwise invalid token returnscode: "token_invalid". Use this to decide whether to refresh or re‑login. POST /api/auth/logoutrevokes the current access token.
Two‑step sign‑in (/api/auth/lookup)
The web console signs in mobile‑first: it asks for the number, then decides what to ask for second. POST /api/auth/lookup is that decision, and it verifies no credential.
curl -X POST https://api.telenow.ai/api/auth/lookup \
-H "Content-Type: application/json" \
-d '{ "identifier": "+919876543210" }'
identifier also accepts an email, and the field may be sent as phone, mobile or email — they are aliases of the same field.
{
"success": true,
"data": {
"status": "password",
"identifier": "+919876543210",
"channel": "phone",
"selfSignupEnabled": true
}
}
status | Meaning |
|---|---|
password | An account exists — ask for the password, then POST /api/auth/login |
signup | Nobody holds this identifier — start signup with it prefilled |
unavailable | Held by an account that cannot sign in here (deactivated, or a partner‑plane staff identity) |
Sign in and sign up with the identifier this returns, not the raw input: it is normalized server‑side (+<digits> for a mobile, lower‑cased for an email) and is the exact string the account is keyed on.
Throttled per address. This endpoint deliberately reveals whether an identifier is registered — that is what makes the two‑step page possible — but nothing beyond that, and password attempts remain rate‑limited per account independently.
Sign in with a one‑time code (/api/auth/login-otp)
For the returning user who doesn't remember their password. Unlike a reset, this issues a session rather than forcing a new password to be invented and stored.
curl -X POST https://api.telenow.ai/api/auth/login-otp \
-H "Content-Type: application/json" \
-d '{ "identifier": "+919876543210" }'
{
"success": true,
"data": {
"requestId": "6d25aecb-…",
"channel": "phone",
"destinationHint": "+9198••••••10",
"expiresInMinutes": 10
}
}
The code goes to the identifier you submitted — never to the account's other channel. Redeem it:
curl -X POST https://api.telenow.ai/api/auth/login-otp/verify \
-H "Content-Type: application/json" \
-d '{ "requestId": "6d25aecb-…", "code": "482913" }'
Answers data.status: "login" with the same { user, tokens } envelope password login returns — or data.status: "mfa_required" for an MFA‑enabled account, in which case re‑post the same code with totpCode. A code emailed to an inbox is exactly the factor TOTP exists to backstop, so this path does not skip it; the challenge does not consume the code.
POST /api/auth/login-otp/resend mints a fresh code for the same requestId after a 60‑second cooldown. It does not reset the wrong‑guess counter, so resending cannot refill the guess budget.
Codes live 10 minutes, are single‑use, are stored only as a SHA‑256 hash, and die after 5 wrong guesses. Sends are capped at 5 per destination per hour. The endpoint answers identically — and in the same time — whether or not the identifier has an account.
The X-Org-Id header
Organization‑scoped endpoints live under /api/orgs/{orgId}/… (plus a few others like POST /api/sessions/initiate-call).
With a user JWT, send X-Org-Id: {orgId}. Telenow checks you're a member of that org before the handler runs, and the handler independently re‑checks your membership of the {orgId} in the path — so a mismatched header and path can't widen your access beyond orgs you already belong to. In practice: always set the header to the same org as the path.
With an API key, the org is implicit — it comes from the key, and X-Org-Id is ignored. The key is additionally pinned to its own org: if the {orgId} in the path isn't the key's org, the request fails with 403 — "This API key is scoped to a different organization." This holds even when the person who created the key belongs to several orgs, so a key can never be pointed at an org it wasn't minted for.
JWTs are never accepted on /api/v1/*.
Client tokens (for browsers and mobile apps)
A client token is an ephemeral, single‑call credential your backend mints with its API key and hands to an untrusted client:
your server --X-API-Key--> POST /api/client-tokens -> { token, expiresAt }
|
+-- token --> browser --Bearer--> POST /api/sessions/init-web-call -> { sessionId, websocketUrl }
Everything that costs money or grants access — which agent, which context variables, who the caller is — is baked in at mint time and re‑read from the token at redemption. The browser cannot change any of it, even by editing the request. Tokens default to a 10‑minute lifetime and a single call.
Full reference: Client tokens. Ready‑made helpers: telenow.clientTokens.create() in the server SDKs.
All auth & account endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/auth/register | — | Create a user account |
POST | /api/auth/lookup | — | Does this mobile/email have an account? → password | signup | unavailable |
POST | /api/auth/login | — | Email or mobile + password login → { user, tokens } |
POST | /api/auth/login-otp | — | Send a one‑time sign‑in code to an email or mobile |
POST | /api/auth/login-otp/verify | — | Redeem the code → { user, tokens } (or mfa_required) |
POST | /api/auth/login-otp/resend | — | Re‑send the code (60s cooldown) |
POST | /api/auth/refresh | — | Exchange a refresh token for new tokens |
POST | /api/auth/forgot-password | — | Request a password‑reset email |
POST | /api/auth/reset-password | — | Complete reset with the emailed token |
POST | /api/auth/oidc/google/callback | — | Sign in with Google (OIDC) |
POST | /api/auth/mfa/challenge | — | Complete an MFA‑gated login (email + password + TOTP code) |
GET | /api/auth/me | JWT | Current user profile |
POST | /api/auth/change-password | JWT | Change password |
POST | /api/auth/logout | JWT | Revoke the current access token |
POST | /api/auth/mfa/setup | JWT | Begin MFA enrolment (returns a QR/secret) |
POST | /api/auth/mfa/verify | JWT | Confirm MFA enrolment with a TOTP code |
POST | /api/auth/mfa/disable | JWT | Disable MFA (requires current password) |
POST | /api/auth/deactivate | JWT | Deactivate your own account |
MFA login flow
POST /api/auth/loginwithemail+password.- If the account has MFA enabled and you didn't send a code, the response is
401witherror: "mfa_required"(no tokens are issued). - Complete the login one of two ways — both return the same
{ user, tokens }payload:- Retry the same call:
POST /api/auth/loginwithemail,password, andtotpCode(the 6‑digit code), or - Use the challenge endpoint:
POST /api/auth/mfa/challengewith{ "email": "…", "password": "…", "code": "123456" }.
- Retry the same call:
Most integrations should use API keys and skip the user‑login flow entirely.
Which should I use?
| Use case | Auth |
|---|---|
Automation platform (Zapier/Make/n8n) — /api/v1 | API key (required) |
| Backend integration, CI, scripts | API key |
| Managing agents programmatically | API key (owner/admin/developer role) |
| Acting as a specific signed‑in user / building a custom dashboard | User JWT |
| Voice call from your own web or mobile app | Client token, minted server‑side |
| Anonymous public page, no backend | Neither — use the public widget |
| Minting or revoking API keys | User JWT (keys can't manage keys) |
Tips
- The
X-API-Keyheader is case‑insensitive (HTTP header names always are), but use the full secret value — don't truncate to the last four digits shown in the dashboard. - Rotate keys by creating a new one, deploying it, then revoking the old one — there's no in‑place rotation.
- Give each integration its own key so you can revoke one without breaking the others, and so the audit log attributes actions to the right key.