Backend (Node & Python)
Backend SDKs (Node & Python)
Drive Telenow from your own backend: place phone calls, mint browser/app sessions, transfer and end live calls, verify webhooks, manage agents, and build Custom API endpoints that stream your own LLM into a call. Both SDKs are zero-dependency wrappers over the same REST API.
npm i @telenow/server # Node 18+ / Bun / Deno / Cloudflare Workers
pip install telenow # Python 3.8+ ("telenow[django]" adds the Django helper)
Before you start
- An agent (dashboard → Agents) and its agent ID.
- An org API key — dashboard → Developers → API keys → New key (the
secret
vai_live_…is shown once). It's sent as theX-API-Keyheader; keep it server-side only. You need a developer, admin, or owner role to mint keys. - Phone calls additionally need a phone number attached to the agent (dashboard → Numbers).
The same key also works against the
/api/v1automation surface (flat JSON, read/chat/webhook endpoints) — handy when you want a quick HTTP call without the SDK, or are wiring Telenow into Zapier/Make/n8n. The SDKs wrap the most-used parts of it directly — including text chat (tn.chat.send/tn.chat(...)).
import { Telenow } from '@telenow/server';
const tn = new Telenow({ apiKey: process.env.TELENOW_API_KEY }); // baseUrl defaults to api.telenow.ai
import os
from telenow import Telenow
tn = Telenow(api_key=os.environ["TELENOW_API_KEY"])
Place a phone call
const { sessionId, callId, status } = await tn.calls.create({
agentId: 'AGENT_UUID',
to: '+15551234567', // E.164
variables: { order_id: 'A-1042' }, // context variables
identifier: 'customer-9', // trusted caller identity for tools
firstResponse: 'Hi! Calling about your delivery.', // optional opener override
machineDetection: 'true', // 'true' = auto-voicemail, 'hangup'
});
result = tn.create_call(
"AGENT_UUID", "+15551234567",
variables={"order_id": "A-1042"},
identifier="customer-9",
first_response="Hi! Calling about your delivery.",
machine_detection="true",
)
| Field (Node / Python) | Required | What it does |
|---|---|---|
agentId / first arg | ✓ | Which agent makes the call. |
to / second arg | ✓ | Destination number, E.164. |
variables | if the agent requires them | Context variables substituted into the prompt. |
identifier | — | Trusted caller identity, injected into tool calls when the agent opts in. |
firstResponse / first_response | — | Override the agent's opener for this one call. |
machineDetection / machine_detection | — | Answering-machine handling: 'true' auto-voicemail, 'hangup' drop on machine. Plivo, Twilio, Vobiz, and Vonage outbound only (the per-dial field is ignored on Exotel and SIP-trunk). |
Manual / softphone calls
Place a human call instead of an AI one — the building block for
click-to-call inside your own CRM or helpdesk. Telenow rings the customer
(to) from a caller-ID your org owns (from) and bridges the carrier leg to a
softphone running in your frontend (the web /
React Native client SDK). No AI, STT, LLM, or TTS runs — a
person is on the line. It works on every carrier (Plivo, Twilio, Vobiz,
Exotel, Vonage, SIP trunks); BYOC numbers dial on your own carrier account.
Two steps — your backend mints the session, your frontend connects the audio:
// 1) Backend: mint the softphone session (the API key never leaves the server).
app.post('/crm/dial', async (req, res) => {
const session = await tn.calls.createManual({
to: req.body.customerPhone, // E.164 number to ring
from: '+15550001111', // your org's caller-ID (Numbers / BYOC / SIP)
userId: req.body.agentUserId, // optional attribution: who placed the call
});
res.json(session); // { sessionId, websocketUrl, callId, callMode: 'manual', fromNumber, toNumber }
});
@app.post("/crm/dial")
async def crm_dial(req):
return tn.create_manual_call(
req.customer_phone, # E.164 number to ring
from_number="+15550001111", # your org's caller-ID (Numbers / BYOC / SIP)
user_id=req.agent_user_id, # optional attribution
)
// 2) Frontend: connect the softphone — this becomes the rep's mic + speaker.
import { TelenowCall } from '@telenow/client';
const session = await fetch('/crm/dial', { method: 'POST', /* … */ }).then((r) => r.json());
const call = new TelenowCall({ session });
await call.start(); // mic permission → bridged to the customer over the carrier
| Field (Node / Python) | Required | What it does |
|---|---|---|
to / first arg | ✓ | Destination number to ring, E.164. |
from / from_number | ✓ with an API key | Caller-ID — an E.164 number your org owns (Numbers, BYOC, or a SIP trunk). On a user JWT it defaults to the member's allocated number. |
userId / user_id | — | Attribution: the CRM user placing the call. |
The returned sessionId works with calls.transfer / calls.end just like any
other call. Manual calls are recorded server-side (both legs mixed) and fire
the same webhooks as AI calls — so call.ended and
recording.ready deliver the recording URL and call data straight to your CRM.
For the raw REST shape and the carrier bridge mechanics, see the
Web-call API (mode: "manual") and
Making & receiving calls.
Mint a web session for your frontend
The recommended browser/app flow: mint here, return { sessionId, websocketUrl } to your client, and the web /
React Native SDK connects — the API key never reaches the
client, and server-set variables can't be tampered with.
app.post('/voice/session', async (_req, res) => {
res.json(await tn.calls.createWeb({
agentId: 'AGENT_UUID',
variables: { customer_name: 'Sarah' },
firstResponse: 'Hi Sarah! How can I help today?', // optional per-session opener override
}));
});
@app.post("/voice/session")
async def voice_session():
return tn.init_web_call(
"AGENT_UUID",
variables={"customer_name": "Sarah"},
first_response="Hi Sarah! How can I help today?", # optional per-session opener override
)
firstResponse / first_response overrides the agent's opening line for this
web session only (the same field as on a phone call) — the agent speaks it first,
before the visitor talks. Omit it to use the agent's saved opener; variables
resolve inside it ("Hi {customer_name}!").
Control a live call
Works for phone and web calls — the client SDKs expose call.sessionId
for exactly this:
await tn.calls.transfer(sessionId, '+15557654321'); // warm transfer to a human
await tn.calls.end(sessionId); // hang up
tn.transfer_call(session_id, "+15557654321")
tn.end_call(session_id)
Text chat (Chat API)
Run a text conversation with an agent over plain REST — same brain,
knowledge bases (RAG), and HTTP tools as a voice call,
no audio and no RAG pipeline of your own to build. Chats settle as chat
calls and fire the same webhooks as
voice. Full protocol + error catalog: Chat API ·
chat bot guide.
The protocol: omit sessionId on the first turn (a session is created and
returned), then pass it on every follow-up. identifier is your stable
end-user id (1–128 chars) — it binds the session so a leaked sessionId can't
be hijacked. A 410 means the session expired (resend the same message
without sessionId to restart); a 409 means a reply is still generating
(wait, then retry).
// Low-level: one turn at a time.
const first = await tn.chat.send({ agentId: 'AGENT_UUID', identifier: 'user-42', input: 'Hello!' });
// → { sessionId, reply, turn: 1, identifier }
const next = await tn.chat.send({ agentId: 'AGENT_UUID', identifier: 'user-42', input: 'Tell me more', sessionId: first.sessionId });
const { messages } = await tn.chat.messages(first.sessionId); // transcript
await tn.chat.end(first.sessionId); // settle now (idempotent)
first = tn.chat("AGENT_UUID", "user-42", "Hello!") # → {"sessionId", "reply", "turn", ...}
nxt = tn.chat("AGENT_UUID", "user-42", "Tell me more", session_id=first["sessionId"])
msgs = tn.chat_messages(first["sessionId"])["messages"] # transcript
tn.chat_end(first["sessionId"]) # settle now (idempotent)
Or skip the bookkeeping with the built-in send-loop helper, which holds the
sessionId, restarts on 410, and waits out 409 for you — keep one per end
user:
import { chatLoop } from '@telenow/server';
const convo = chatLoop(tn, { agentId: 'AGENT_UUID', identifier: 'user-42', variables: { plan: 'Pro' } });
const a = await convo.send('Hello!'); // turn 1
const b = await convo.send('What are your hours?'); // turn 2 (or a transparent restart)
await convo.end();
convo = tn.chat_conversation("AGENT_UUID", "user-42", variables={"plan": "Pro"})
a = convo.send("Hello!") # turn 1
b = convo.send("What are your hours?") # turn 2 (or a transparent restart)
convo.end()
Replies are synchronous (the HTTP call returns when the agent's full reply, including tool calls, is ready) — use a generous client timeout (60 s+). Send one turn at a time per
sessionId. Voice-only native tools (transfer, hangup) don't run in chat.
Verify webhooks
Telenow signs every delivery with X-VoiceAI-Signature: sha256=<hex>
(HMAC-SHA256 of the raw body). Verify before trusting the payload — both
SDKs use a constant-time compare:
app.post('/webhooks/telenow', express.raw({ type: 'application/json' }), async (req, res) => {
const ok = await tn.webhooks.verify(req.body, req.get('x-voiceai-signature') ?? '', SECRET);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString()); // call.started | call.ended | transcript.ready | tool.invoked
res.status(200).end();
});
from telenow.django import telenow_webhook
@telenow_webhook(secret=settings.TELENOW_WEBHOOK_SECRET)
def hook(request, event): # signature verified, body parsed
if event["event"] == "call.ended":
...
return HttpResponse(status=200)
(Non-Django Python: telenow.verify_webhook(raw_body, header, secret) against
the raw bytes.) Configure endpoints + events per agent or org-wide in the
dashboard; payload shapes in the
webhook events reference.
Custom API — stream your own LLM into a call
When an agent's Brain is set to Custom API, Telenow
runs STT + TTS and POSTs each user turn to your endpoint
(?calling=true&stream=true, body { query, userId?, ...payload }), then
speaks your Server-Sent-Events reply as tokens arrive. The SDK helpers emit
the exact wire format:
import { customApiNodeHandler, callEnd } from '@telenow/server';
app.post('/telenow-llm', customApiNodeHandler(async function* ({ query, userId }) {
for await (const token of myLlm.stream(query)) yield token; // spoken as it streams
// yield callEnd('Goodbye!'); // optional hangup after the reply
}, { bearer: process.env.TELENOW_BEARER }));
// Fetch-style runtimes (Next.js routes / Bun / Workers / Deno): customApiFetchHandler(handler)
from telenow import custom_api
from fastapi.responses import StreamingResponse
@app.post("/telenow-llm")
async def llm(request: Request):
body = await request.json()
async def gen():
async for token in my_llm_stream(body["query"]):
yield token # str → spoken token delta
# yield custom_api.call_end("Goodbye!") # dict → control event
return StreamingResponse(custom_api.sse_astream(gen()),
media_type=custom_api.MEDIA_TYPE,
headers=custom_api.SSE_HEADERS)
# Django/Flask (sync): wrap a generator with custom_api.sse_stream(...) instead.
Three things that save debugging time:
- Yield strings for spoken tokens, objects/dicts for control events; the
[DONE]terminator is appended for you — even if your generator throws. - The provided headers disable proxy buffering (
X-Accel-Buffering: no) — without them, nginx batches your tokens and the agent speaks in one late burst. - Hangup is an SSE event (
callEnd()/custom_api.call_end()); transfer is a REST action — callcalls.transfer(sessionId, to)from inside your handler.
Errors
Non-2xx responses throw/raise TelenowError with .status and .body;
successful responses are unwrapped from the { success, data } envelope.
| Status | Usual cause |
|---|---|
| 401 / 403 | Wrong/revoked API key, or the agent's API access toggle is off (Publish tab). |
| 400 | Missing required field — often a required context variable or a non-E.164 number. |
| 404 | Wrong agent/session id, or the session already ended. |
Agents & everything else
agents.list/get/create/update (Node) and
list_agents/get_agent/create_agent/update_agent (Python) mirror the
Agents API. Anything not wrapped: call the REST API
directly with the same key.
Rust, cURL & other languages
Call the plain REST API — every endpoint is documented with curl in the
API reference, and your agent's Publish tab shows
ready-to-paste cURL, Node, Python, and Rust samples with your agent id
filled in. You can also generate a typed client from our OpenAPI spec with
openapi-generator (Go, Ruby, PHP, C#…).