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 agentThat agent
Held by a team member for inbound callsThe team member, then the agent or a missed-call message. See Phone numbers → Inbound routing
A pool numberWhoever your routing endpoint names, for this call. The number's agent is the fallback

For each call to a pool number:

  1. Telenow sends your routing endpoint the call's numbers, signed, and waits up to your timeout (1.2 seconds by default).
  2. Your endpoint answers: route the call, to an agent you name or to the number's own, or reject it.
  3. Telenow checks the answer. An answer that fails a check is ignored, and the number's own agent answers, with no variables or overrides.
  4. 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.
  5. 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

  1. Assign the fallback agent to the number: POST /api/voice/numbers/{id}/assign-agent. It answers every call your endpoint doesn't route.
  2. Mark the number as a pool: POST /api/voice/numbers/{id}/pool.
  3. Tell Telenow where your routing endpoint is: PUT /api/orgs/{orgId}/inbound-routing. Keep the signing secret it returns.
  4. 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 }'
FieldTypeNotes
isPoolbooleanRequired. true marks the number as a pool, false unmarks it
maxConcurrentinteger | nullThe 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".
  • maxConcurrent must be a positive whole number or null; anything else is a 400. A released number, or one in another organization, is a 404.
  • 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 }'
FieldTypeNotes
urlstringRequired. Your routing endpoint. https only, on a public address: loopback, link-local and private ranges are refused (400 url rejected: …)
timeoutMsintegerHow 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
enabledbooleanDefault true, or the current value on an update. false keeps the configuration but stops asking: every call goes to the number's own agent
rotateSecretbooleantrue 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"
  }
}
  • signingSecret is shown once, in the answer that minted it: on first setup, or with rotateSecret: true. Every other answer carries null and the last four characters in signingSecretHint. 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-routing returns the same object, without the secret, or data: null when nothing is configured. Any member of the organization can read it.
  • DELETE /api/orgs/{orgId}/inbound-routing removes 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: false means a real call would have gone to fallbackAgentId, and reason says why: the same text lastError records.
  • 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 toNumber that isn't one of your pool numbers is a 400.

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": "…" }
FieldMeaning
to_numberThe pool number the call arrived on, in E.164
caller_numberThe caller, in E.164. null when the caller ID is withheld
forwarded_fromThe line that forwarded the call, exactly as the carrier reported it, or null when the carrier didn't say. Cut at 64 characters
carrierplivo, twilio, sip, exotel, vobiz, vonage or smartflo; test from the test endpoint
call_idThe 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.

CarrierWhere forwarded_from comes from
Plivo, TwilioThe answer webhook's ForwardedFrom
SIP trunksThe Diversion or History-Info header
Exotel, Vobiz, Vonage, Tata Tele SmartfloNot 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 }
}
KeyTypeMeaning
actionstringroute (the default) or reject. See Rejecting a call
agent_iduuidThe 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
variablesobjectValues 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
overridesobjectThis call's voice, language, opening line and more. See Overrides
limits.max_duration_secsintegerA 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.

KeyUp toWhat it does
tts_voice128 charsThe voice for this call
tts_provider128 charsThe 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_model128 charsThe model behind the voice, for providers whose voices need one. Needs tts_voice
language32 charsA language tag such as hi-IN, for speech recognition and the voice alike. Letters, digits and hyphens only
opener500 charsThe 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_fields30 fieldsThis 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 than 2xx;
  • a body over 64 KB, or one that isn't a JSON object;
  • an action other than route or reject;
  • an agent_id that 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 variables that isn't an object or a non-numeric max_duration_secs;
  • a value past its limit: more than 64 variables, an opener over 500 characters, max_duration_secs outside 15–14,400;
  • a language that isn't a language tag, a persona_gender that isn't one of the three, or a tts_provider or tts_model without a tts_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: true means there were more.
  • Only Plivo numbers are supported today. Another carrier's number answers supported: false with no calls.
  • Needs the owner, admin or developer role.