@telenow/server
v0.1.4
Published
Telenow voice AI SDK for Node, Bun, Deno & edge — place AI agent phone calls, mint browser/app call sessions, transfer & end live calls, verify webhooks, and build custom-LLM streaming (SSE) endpoints. Zero dependencies.
Maintainers
Readme
@telenow/server
Telenow voice AI SDK for Node 18+ / Bun / Deno /
Cloudflare Workers. Zero dependencies (global fetch + Web Crypto). From
your backend you can: place AI agent phone calls, mint browser/app call
sessions, transfer/end live calls, verify webhooks, manage agents,
and build Custom API (bring-your-own-LLM) streaming endpoints.
npm install @telenow/serverBefore you start
- A Telenow account + agent (dashboard → Agents). Copy the agent ID from the agent page (or its Publish tab samples).
- An org API key — dashboard → Developers tab. Keep it server-side
only; it's sent as the
X-API-Keyheader. - Phone calls additionally need a phone number attached to the agent (dashboard → Numbers).
import { Telenow } from '@telenow/server';
const tn = new Telenow({
apiKey: process.env.TELENOW_API_KEY!, // required
baseUrl: 'https://api.telenow.ai', // default — override for self-hosted
// fetch: customFetch, // optional fetch override (Node < 18 polyfill)
});Phone calls
const { sessionId, callId, status } = await tn.calls.create({
agentId: 'AGENT_UUID',
to: '+15551234567', // E.164
variables: { order_id: 'A-1042' }, // context variables (required ones must be present)
identifier: 'customer-9', // trusted caller identity for the agent's tools
firstResponse: 'Hi! Calling about your delivery.', // optional opener override for THIS call
machineDetection: 'true', // 'true' = auto-voicemail, 'hangup' (Plivo)
});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 upManual / softphone calls (embed click-to-call in your CRM)
Place a human call, not an AI one: Telenow rings to from your org's from
caller-ID and bridges the carrier leg to a softphone in the browser/app. No
AI, STT, LLM, or TTS — a person is on the line. This is the building block for
click-to-call inside a CRM.
// 1) Your backend mints the softphone session (API key stays server-side).
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: who placed the call (attribution)
});
res.json(session); // { sessionId, websocketUrl, callId, callMode: 'manual', fromNumber, toNumber }
});// 2) Your frontend connects the softphone — 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
// call.setMuted(true); call.stop(); — same controls as any web call| createManual field | Required | What it does |
|---|---|---|
| to | ✓ | Destination number to ring, E.164. |
| from | ✓ with an API key | Caller-ID — an E.164 number your org owns (Numbers / BYOC / SIP trunk). On a user JWT it defaults to the member's allocated number. |
| userId | — | Attribution: the CRM user placing the call. |
Manual calls are recorded server-side (both legs mixed) and fire the same
webhooks as AI calls — so the
call.ended / recording.ready events deliver the recording URL and call data
straight to your CRM (see Webhooks below). Works on every carrier — Plivo,
Twilio, Vobiz, Exotel, Vonage, and SIP trunks. transfer/end work on the
returned sessionId too.
Web calls (mint a session for your frontend)
The recommended browser/app flow: mint here, hand the result to the client, the client SDK connects — the API key never reaches the browser.
app.post('/voice/session', async (_req, res) => {
const session = await tn.calls.createWeb({
agentId: process.env.AGENT_ID!,
variables: { customer_name: 'Asha' }, // baked in server-side — client can't tamper
identifier: 'customer-9',
firstResponse: 'Hi Asha! How can I help today?', // optional: override the opener for THIS session
});
res.json(session); // { sessionId, websocketUrl } → TelenowCall({ session })
});Text chat (Chat API)
Run a text conversation with an agent — same brain, knowledge bases (RAG)
and HTTP tools as a voice call, no audio, no RAG pipeline of your own. Chats
settle as chat calls in history and fire the same webhooks as voice.
Omit sessionId on the first turn (one is created + returned), then pass it on
follow-ups. identifier is your stable end-user id (binds the session). A 410
means the session expired (resend without sessionId); 409 means a reply
is still generating (wait, retry).
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: 'More', sessionId: first.sessionId });
const { messages } = await tn.chat.messages(first.sessionId); // full transcript
await tn.chat.end(first.sessionId); // settle now (idempotent)Or let the send-loop helper hold the sessionId, restart on 410, and wait
out 409 for you — 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();Replies are synchronous (return when the agent's full reply, incl. tool calls,
is ready) — use a 60 s+ timeout, and send one turn at a time per sessionId.
Full reference: Chat API.
Webhooks
Telenow signs every delivery with X-VoiceAI-Signature: sha256=<hex>
(HMAC-SHA256 of the raw body). Always verify before trusting the payload:
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());
switch (event.event) {
case 'call.started': break; // sessionId, agentId, from/to, timestamps
case 'call.ended': break; // + duration, optional recording URL & transcript
case 'recording.ready': break; // signed recording URL (may land just after the call)
case 'transcript.ready': break; // full transcript
case 'tool.invoked': break; // tool name, input, result
}
res.status(200).end();
});Configure endpoints + which events to receive in the dashboard (Webhooks) or
the REST-hooks API, per agent or org-wide. Tick include recording on the
endpoint to get the signed recording URL on call.ended. These fire for AI
agent calls, web calls, and manual/softphone
calls alike — one receiver ingests them all. Payload shapes:
webhook events reference.
Custom API — stream your own LLM into calls
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, JSON
body { query, userId?, ...payload }), then speaks your Server-Sent-Events
reply as tokens arrive. These helpers emit the exact wire format:
import { customApiNodeHandler, callEnd } from '@telenow/server';
// Express or node:http
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: hang up after reply
}, { bearer: process.env.TELENOW_BEARER })); // 401s anything without your bearerimport { customApiFetchHandler } from '@telenow/server';
// Next.js route handler / Bun.serve / Workers / Deno
export const POST = customApiFetchHandler(async function* ({ query }) {
yield `You said: ${query}`;
});Notes that save debugging time:
- Yield strings for spoken token deltas; yield
callEnd(msg?)(or any{ type: ... }object) for control events. The helper appends the SSE[DONE]terminator — even if your generator throws midway, so the turn ends cleanly. - Each event is written as one chunk and the provided headers disable
proxy buffering (
X-Accel-Buffering: no) — without that, nginx batches your tokens into one late burst. - Transfer is a REST action, not an SSE event: call
tn.calls.transfer(sessionId, to)from inside your handler. - Lower-level pieces are exported too:
customApiStream,sseEncode,SSE_DONE,SSE_HEADERS.
Agents & everything else
tn.agents.list() / get(id) / create(data) / update(id, data) mirror the
Agents API. Anything not wrapped yet:
call the REST API with the same
X-API-Key header.
Errors
Every non-2xx (and { success: false } envelope) throws TelenowError:
import { TelenowError } from '@telenow/server';
try {
await tn.calls.create({ agentId, to });
} catch (e) {
if (e instanceof TelenowError) console.error(e.status, e.message, e.body);
}| 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 agentId / sessionId, or the session already ended. |
Successful responses are unwrapped from the { success, data } envelope — you
get the data object directly.
What is Telenow?
Telenow is a voice AI platform for building production-grade phone and web agents. Pick a brain from the built-in LLM/STT/TTS providers (or bring your own model and carrier), give the agent a prompt, tools, and knowledge, and put it on a phone number, your website, or your app. Every call comes with recordings, transcripts, analytics, warm transfer to humans, outbound campaigns, and webhooks.
- Website: telenow.ai
- Documentation: telenow.ai/docs
- This SDK's guide: telenow.ai/docs/sdk-server
