Pool numbers & inbound routing
A phone number normally answers with the one agent assigned to it. A pool number is a number many people forward their calls to: a call-screening or answering service, say, where every subscriber's carrier forwarding points at one shared number. The dialled number is the same for all of them, so on its own it can't say whose line the call came from.
On a pool number, Telenow asks your routing endpoint — a route on your own backend — who should answer each call: which agent, with which variables, voice, language and opening line. If your endpoint doesn't answer in time, the number's own agent answers. Routing can personalise a call; it never drops one.
Pool numbers and the routing endpoint are set up through the API; there is no dashboard screen for them yet.
How an inbound call is routed
| The number is… | Who answers |
|---|---|
| An ordinary number with an agent | That agent |
| Held by a team member for inbound calls | The team member, then the agent or a missed-call message. See Phone numbers → Inbound routing |
| A pool number | Whoever your routing endpoint names, for this call. The number's agent is the fallback |
For each call to a pool number:
- Telenow sends your routing endpoint the call's numbers, signed, and waits up to your timeout (1.2 seconds by default).
- Your endpoint answers: route the call, to an agent you name or to the number's own, or reject it.
- Telenow checks the answer. An answer that fails a check is ignored, and the number's own agent answers, with no variables or overrides.
- The call still has to fit your organization's live-call cap and monthly quota, and the pool number's own ceiling if you set one. A call that doesn't fit is refused, and the caller hears busy.
- The agent answers with your variables and overrides applied.
A pool number always answers with an agent; it never rings team members. A number answers with its agent or rings a member, not both. Only inbound calls are routed; outbound calls placed from a pool number are unaffected.
Set it up
- Assign the fallback agent to the number:
POST /api/voice/numbers/{id}/assign-agent. It answers every call your endpoint doesn't route. - Mark the number as a pool:
POST /api/voice/numbers/{id}/pool. - Tell Telenow where your routing endpoint is:
PUT /api/orgs/{orgId}/inbound-routing. Keep the signing secret it returns. - Test it with a made-up call:
POST /api/orgs/{orgId}/inbound-routing/test.
Authenticate with an API key, or a user JWT. The number endpoints also take X-Org-Id with a JWT, like the rest of the Phone numbers API. Changes need an API key whose role is owner, admin or developer; on /inbound-routing a signed-in user needs one of those roles too.
Mark a pool number
curl -X POST https://api.telenow.ai/api/voice/numbers/{id}/pool \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "isPool": true, "maxConcurrent": null }'
| Field | Type | Notes |
|---|---|---|
isPool | boolean | Required. true marks the number as a pool, false unmarks it |
maxConcurrent | integer | null | The pool number's own ceiling on simultaneous calls. null (the default) means no per-number ceiling. Ignored, and cleared, when unmarking |
The answer is the updated number, with is_pool and pool_max_concurrent.
- The number must have an agent. Marking a number with no agent is a
400: "assign this number to an agent first — a pool number's agent is the fallback every call lands on when the routing endpoint does not answer". maxConcurrentmust be a positive whole number ornull; anything else is a400. A released number, or one in another organization, is a404.- Concurrency. An ordinary number carries 2 simultaneous calls by default, the channels one business line has (see Concurrency limits). A pool number answers for many forwarders, so it has no per-number ceiling unless you set
maxConcurrent. Your organization's live-call cap still applies. - A change applies within 30 seconds on every server.
Configure the routing endpoint
curl -X PUT https://api.telenow.ai/api/orgs/{orgId}/inbound-routing \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "url": "https://api.example.com/telenow/route", "timeoutMs": 1200 }'
| Field | Type | Notes |
|---|---|---|
url | string | Required. Your routing endpoint. https only, on a public address: loopback, link-local and private ranges are refused (400 url rejected: …) |
timeoutMs | integer | How long Telenow waits for your answer: 200–3000, default 1200. It runs while the caller's call is being answered, so every millisecond is silence on the line |
enabled | boolean | Default true, or the current value on an update. false keeps the configuration but stops asking: every call goes to the number's own agent |
rotateSecret | boolean | true mints a new signing secret. One is always minted on first setup |
{
"success": true,
"data": {
"orgId": "…",
"url": "https://api.example.com/telenow/route",
"timeoutMs": 1200,
"enabled": true,
"signingSecret": "ir_secret_…",
"signingSecretHint": "…x7Qk",
"lastOkAt": null,
"lastError": null,
"lastErrorAt": null,
"createdAt": "2026-10-07T09:00:00Z",
"updatedAt": "2026-10-07T09:00:00Z"
}
}
signingSecretis shown once, in the answer that minted it: on first setup, or withrotateSecret: true. Every other answer carriesnulland the last four characters insigningSecretHint. A new secret reaches every server within a minute, so accept both the old and the new one for that minute.GET /api/orgs/{orgId}/inbound-routingreturns the same object, without the secret, ordata: nullwhen nothing is configured. Any member of the organization can read it.DELETE /api/orgs/{orgId}/inbound-routingremoves the configuration ({ "removed": true }). Pool numbers then answer with their own agents.- A change applies at once on the server that took it, and within a minute everywhere.
Health. lastOkAt is the last time your endpoint gave a usable answer. lastError and lastErrorAt are the last failure, in the words the test uses, such as timed out after 1200 ms or answer refused: agent … does not allow telephony. A success doesn't clear lastError: compare the two times. They are written at most every 15 seconds while nothing changes, and at once when the outcome flips.
Test it
Runs one made-up call through the same path a real call takes, and reports what Telenow would do. No call is placed and no session is created.
curl -X POST https://api.telenow.ai/api/orgs/{orgId}/inbound-routing/test \
-H "X-API-Key: vai_live_…" -H "Content-Type: application/json" \
-d '{ "toNumber": "+918000000001", "callerNumber": "+919000000001", "forwardedFrom": "09876543210" }'
toNumber must be one of your pool numbers; callerNumber and forwardedFrom are optional. forwardedFrom is sent as you typed it, the way a carrier's value is sent as reported.
{
"success": true,
"data": {
"request": { "to_number": "+918000000001", "caller_number": "+919000000001", "forwarded_from": "09876543210", "carrier": "test", "call_id": "test-…" },
"answered": true,
"elapsedMs": 182,
"reason": null,
"decision": {
"action": "route",
"agentId": "…",
"maxDurationSecs": 600,
"variables": { "subscriber_name": "Ramesh" },
"overrides": { "ttsVoice": null, "ttsProvider": null, "ttsModel": null, "language": "hi-IN", "opener": "Namaste, Ramesh ji ka phone hai.", "personaGender": "female" }
},
"fallbackAgentId": "…"
}
}
answered: falsemeans a real call would have gone tofallbackAgentId, andreasonsays why: the same textlastErrorrecords.- The test checks the answer exactly as a live call does, so an agent from another organization shows as refused here instead of on a real call.
- The post-call analysis overrides are applied on real calls but are not shown in
decision. - A
toNumberthat isn't one of your pool numbers is a400.
The request Telenow sends
One POST per inbound call to a pool number:
POST /telenow/route HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-Telenow-Event: inbound.route
X-Telenow-Signature: 5d2c…e91f
{ "to_number": "+918000000001", "caller_number": "+919000000001", "forwarded_from": "09876543210", "carrier": "plivo", "call_id": "…" }
| Field | Meaning |
|---|---|
to_number | The pool number the call arrived on, in E.164 |
caller_number | The caller, in E.164. null when the caller ID is withheld |
forwarded_from | The line that forwarded the call, exactly as the carrier reported it, or null when the carrier didn't say. Cut at 64 characters |
carrier | plivo, twilio, sip, exotel, vobiz, vonage or smartflo; test from the test endpoint |
call_id | The carrier's id for this call |
forwarded_from is never reshaped. From Indian networks, the same line arrives as +919876543210, 919876543210, 09876543210 or 9876543210. Telenow can't know which country a bare national number belongs to, and a + added in front of 09876… would be a number that exists nowhere, so it passes what the carrier sent. Normalise it against your own country: for Indian mobiles, for example, compare the last ten digits.
| Carrier | Where forwarded_from comes from |
|---|---|
| Plivo, Twilio | The answer webhook's ForwardedFrom |
| SIP trunks | The Diversion or History-Info header |
| Exotel, Vobiz, Vonage, Tata Tele Smartflo | Not reported: your endpoint is still asked, with forwarded_from: null |
The connection is made over https to a public address only, redirects are not followed, and connecting must take under 800 ms.
Verify the signature
X-Telenow-Signature is the HMAC-SHA256 of the raw request body, keyed with your routing signing secret, as lowercase hex with no prefix. Compute it over the bytes you received, before parsing them, and compare in constant time:
// Node (Express)
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.TELENOW_ROUTING_SECRET; // ir_secret_…
app.post('/telenow/route', express.raw({ type: 'application/json' }), async (req, res) => {
const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const got = req.get('X-Telenow-Signature') || '';
if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.status(401).end();
}
const call = JSON.parse(req.body);
const sub = await findSubscriber(call.forwarded_from); // your lookup: normalise the number first
if (!sub) return res.json({}); // unknown line: the pool number's own agent answers
if (sub.callsLeftToday === 0) return res.json({ action: 'reject' });
res.json({
agent_id: sub.agentId,
variables: { subscriber_name: sub.name },
overrides: { language: sub.language, opener: `Hello, you've reached ${sub.name}'s assistant.` },
limits: { max_duration_secs: sub.maxCallSecs },
});
});
# Python (Flask)
import hashlib, hmac, os
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
SECRET = os.environ["TELENOW_ROUTING_SECRET"].encode()
@app.post("/telenow/route")
def route():
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Telenow-Signature", "")):
abort(401)
call = request.get_json()
sub = find_subscriber(call["forwarded_from"]) # your lookup: normalise the number first
if sub is None:
return jsonify({}) # unknown line: the pool number's own agent answers
return jsonify({"agent_id": sub.agent_id, "variables": {"subscriber_name": sub.name}})
Any answer other than a 2xx, including the 401 above, sends the call to the number's own agent. Answer quickly: look subscribers up from memory or a fast cache, not from a slow service, because the caller waits for you.
Your answer
A JSON object with a 2xx status, at most 64 KB. Every key is optional: {} routes the call to the pool number's own agent.
{
"agent_id": "3d5a1f90-7c2e-4b18-a6d4-91e2f0c74b35",
"variables": { "subscriber_name": "Ramesh", "plan": "gold" },
"overrides": {
"tts_voice": "…",
"tts_provider": "…",
"language": "hi-IN",
"opener": "Namaste, Ramesh ji ka phone hai. Main unki assistant bol rahi hoon.",
"persona_gender": "female",
"analysis_language": "hi-IN"
},
"limits": { "max_duration_secs": 600 }
}
| Key | Type | Meaning |
|---|---|---|
action | string | route (the default) or reject. See Rejecting a call |
agent_id | uuid | The agent to answer. It must be an agent of your organization, not deleted, and allowed to take phone calls. Leave it out to keep the pool number's own agent |
variables | object | Values for the agent's context variables, used in its prompt and opening line. At most 64; each name 1–128 characters; each value a string of up to 8,000 characters, a number, true/false or null. Objects and arrays are refused |
overrides | object | This call's voice, language, opening line and more. See Overrides |
limits.max_duration_secs | integer | A ceiling on this call's length, 15–14,400 seconds. It only ever lowers the agent's own maximum, never raises it |
Unknown keys are ignored.
Variables fill blanks only. A variable the call already has a value for keeps it. Yours are also stored on the call, so the call.started webhook carries them in variables.
Overrides
Each override changes this call only; anything you leave out keeps the agent's own setting.
| Key | Up to | What it does |
|---|---|---|
tts_voice | 128 chars | The voice for this call |
tts_provider | 128 chars | The voice's provider. Needs tts_voice. When it differs from the agent's, the agent's provider-specific voice settings are left behind; only the language carries over |
tts_model | 128 chars | The model behind the voice, for providers whose voices need one. Needs tts_voice |
language | 32 chars | A language tag such as hi-IN, for speech recognition and the voice alike. Letters, digits and hyphens only |
opener | 500 chars | The agent's first line. {name} placeholders are filled from the call's variables, like the agent's own opener, and the agent speaks first |
persona_gender | — | female, male or neutral: the grammatical gender the agent speaks of itself in, which should match the voice. neutral clears the agent's own |
analysis_language | — | The language this call's post-call analysis is written in, as a code (hi-IN) or a name (Hindi) |
analysis_custom_fields | 30 fields | This call's own analysis custom fields, added to the agent's. The same list as initiate-call's analysis.customFields |
analysis_model | — | This call's analysis model, { "provider", "model" }, in place of the agent's. The same as initiate-call's analysis.model |
What makes Telenow ignore an answer
Any of these sends the call to the pool number's own agent, with no variables and no overrides. The reason is recorded in lastError, and the test endpoint shows it:
- no answer within
timeoutMs, a connection failure, or a status other than2xx; - a body over 64 KB, or one that isn't a JSON object;
- an
actionother thanrouteorreject; - an
agent_idthat isn't a uuid, doesn't exist, is deleted, belongs to another organization, or can't take phone calls; - a key of the wrong type, such as
variablesthat isn't an object or a non-numericmax_duration_secs; - a value past its limit: more than 64 variables, an
openerover 500 characters,max_duration_secsoutside 15–14,400; - a
languagethat isn't a language tag, apersona_genderthat isn't one of the three, or atts_providerortts_modelwithout atts_voice.
The three analysis overrides are the exception. One that can't be read is dropped on its own, and the rest of the answer still applies: they shape the report written after the call, never the call itself.
Rejecting a call
{ "action": "reject" } refuses the call outright: the carrier gets a busy refusal, and no session is created. To the caller it sounds like an engaged line. The number's own agent does not answer, because the point of rejecting — a subscriber who has used up today's calls, for example — is that nobody takes the call.
When the carrier never sent the call
A caller says they called, but your endpoint never heard about it. Ask the carrier what it recorded:
curl "https://api.telenow.ai/api/orgs/{orgId}/inbound-routing/carrier-calls?toNumber=%2B918000000001&from=2026-10-07T09:00:00Z&until=2026-10-07T10:00:00Z" \
-H "X-API-Key: vai_live_…"
It lists the carrier's own records of the inbound calls to one of your pool numbers that ended in the window. Each record shows how the call ended and, in telenowSession, the call it became on Telenow, or null when it never reached Telenow.
{
"success": true,
"data": {
"provider": "plivo",
"supported": true,
"toNumber": "+918000000001",
"from": "2026-10-07T09:00:00Z",
"until": "2026-10-07T10:00:00Z",
"calls": [
{
"callUuid": "…",
"fromLast4": "2345",
"callerKeyHash": "2c0ce47f…",
"initiatedAt": "2026-10-07T09:32:05Z",
"answeredAt": null,
"endedAt": "2026-10-07T09:32:31Z",
"durationSecs": 26,
"billDurationSecs": 0,
"ringDurationSecs": 26,
"hangupCause": "Normal Hangup",
"hangupCauseCode": 4000,
"hangupSource": "Caller",
"callState": "COMPLETED",
"telenowSession": null
}
],
"truncated": false
}
}
- The caller's number never appears. Each record carries its last four digits, and
callerKeyHash: the SHA-256 (hex) of its last ten digits, or of all of them when there are fewer. Hash your own caller's number the same way to find their call on a busy pool number. - The window may be at most 2 hours, within the last 89 days (the carrier keeps 90). At most 100 calls are returned;
truncated: truemeans there were more. - Only Plivo numbers are supported today. Another carrier's number answers
supported: falsewith no calls. - Needs the owner, admin or developer role.
Related
- Phone numbers: buying numbers and assigning agents.
- Phone numbers API:
assign-agentand the other number endpoints. - Context variables: the placeholders your
variablesfill. - Post-call analysis: what the analysis overrides change.
- Webhook events:
call.startedcarries the variables you returned. - Concurrency limits: the caps every call still has to fit.