Skip to content

Telephony ​

Where calls arrive from and leave through: numbers bought through a managed carrier account, numbers you already own, the accounts behind them and the routes that decide which trunk a call leaves by.

For your own carrier trunk, see SIP trunking.

What each carrier can do ​

Nine provider ids are declared. Six have an adapter; zadarma, didlogic and chime throw a 501 naming themselves and telling you to bring the DID over a SIP trunk instead.

Every adapter declares its own capability matrix, and they differ enormously. Read the matrix, not the name. GET /telephony/providers returns the live version.

TwilioTelnyxPlivoDIDWWBYO SIPCustom SIP
Search available numbers✅✅✅✖✖✖
Purchase / release✅✅✅✖✖✖
List account numbers✅✅✅✅✖◐
Configure number webhooks✅✅✅✖——
Inbound voice✅✅✖✅¹✅✅
Outbound voice✅✅✅✖✅✅
Media streams (reaching the AI pipeline)✅⚠️²✖—✅³✅³
Recording✅✅✖—✅✅
Dual-channel recording✅✅✖—✅✅
Transfer✅✅✖✖✅✅
DTMF✅✅✅✖✅✅
Answering-machine detection✅◐⁴✖✖✖✖
SMS / MMS✅✅✅ (SMS)✖✖✖
Webhook signature✅✅✅✖✅✅
SIP trunking◐⁵✅——✅✅

✅ implemented · ◐ partial · ✖ not implemented · — not applicable

  1. Carried over a CallAgent SIP trunk, not over DIDWW's own API.
  2. Telnyx declares media_streams as implemented and nothing in the codebase builds a Telnyx media-stream instruction. placeOutboundCall() posts a Call-Control webhook URL and stops. A Telnyx call will not reach an assistant today. This is a declared capability that disagrees with the runtime — treat the runtime as authoritative.
  3. Declared implemented, and blocked in practice by the same media handoff that blocks SIP. See SIP and What is not built.
  4. Telnyx exposes AMD only on Call Control legs started with answering_machine_detection set at dial time, never retroactively.
  5. Twilio Elastic SIP Trunking is configured out of band, not through this API.

The practical summary

Twilio is the only carrier proven to carry a call into the AI pipeline. Everything else either declares a capability it cannot deliver end to end, or does not declare it.

DIDWW's own comment explains why its buy path is absent rather than stubbed: a DIDWW order is a multi-step flow (country → city → DID group → order → capacity pool) that needs product decisions nobody has made, and the interface must not offer a Buy button that half-works.

Telephony accounts and credentials ​

An account binds a provider credential to an organization. Credentials are encrypted with a key separate from APP_KEY (CredentialCipher, decision D-004), so rotating the application key does not touch tenant provider secrets.

GET/POST         /api/v1/telephony/accounts
GET/PATCH/DELETE /api/v1/telephony/accounts/{id}
POST             /api/v1/telephony/accounts/{id}/health
GET              /api/v1/telephony/providers

Twilio's credential is account_sid + auth_token; Telnyx's and DIDWW's are api_key. See Quickstart step 2.

Numbers ​

This API is camelCase

The telephony endpoints take camelCase field names (friendlyName, destinationType, sipTrunkId). The assistants, tools and calls endpoints take snake_case. This is an inconsistency in the product; the pages here show whichever the endpoint actually accepts.

Register a number you already own ​

bash
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+40312345901",
    "friendlyName": "Support",
    "assistantId": "01JQ…",
    "capabilities": ["voice"]
  }'

capabilities entries are voice, sms, mms, fax. The row is created with provider: "byo_sip", and giving assistantId (or squadId) sets destination_type and destination_id for you.

POST /telephony/phone-numbers/import is the bulk version: it lists every number on your provider account and creates the rows.

Buy one ​

bash
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers/search" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "twilio", "country": "RO", "type": "local", "limit": 20 }'

provider and a two-letter country are required. type is local, mobile, toll_free or national and defaults to local. Then:

bash
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers/purchase" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "twilio", "number": "+40312345678", "friendlyName": "Support" }'

A purchase is three steps: buy at the carrier, insert the row, push webhooks. If the webhook step fails the number is not rolled back — you own it, so the row stays, with webhooks_in_sync: false and the failure appended to notes. Re-run configure.

Configure and release ​

POST /api/v1/telephony/phone-numbers/{id}/configure   push webhook URLs to the carrier
POST /api/v1/telephony/phone-numbers/{id}/release     release at the carrier
DELETE /api/v1/telephony/phone-numbers/{id}           soft-delete the row only

configure takes no request body. It answers { "configured": true, "webhooks_in_sync": true } or a 502. It does not choose a destination — that is PATCH.

Point a number somewhere ​

bash
curl -X PATCH "$CALLAGENT_API_URL/telephony/phone-numbers/$NUMBER_ID" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destinationType": "assistant",
    "destinationId": "01JQ…",
    "recordingMode": "dual_channel",
    "maxConcurrentCalls": 10
  }'

Destination types, all ten:

TypeNeeds
assistant, squad, queue, ivr, ring_group, extension, voicemaildestinationId
external, sip_uridestinationValue
hookhookUrl and the hook fields below

Other PATCH fields worth knowing: inboundEnabled, outboundEnabled, outboundCallerIdNumber / Name, smsEnabled, smsAssistantId, recordingMode (disabled · mono · dual_channel · inherit), recordingConsentRequired, callerIdPolicyId, status (pending · active · suspended · released · error), metadata.

hookSecret is never assigned through the ordinary path — the column holds ciphertext, and writing a raw secret to the attribute would store it in the clear.

How an inbound call is routed ​

First match wins:

  1. an inbound route matching the number, in priority order;
  2. the number's dynamic hook, if hookEnabled;
  3. the number's static destination;
  4. for a SIP-trunk call only, the trunk's fallback destination.

The number lookup deliberately runs across tenants, because no tenant is bound yet, and the organization is then bound from the row that matched — never from anything in the request. A number that matches nothing is rejected rather than routed to a default.

Dynamic hooks ​

Instead of a fixed destination, a number can ask your server at call time:

FieldMeaning
hookUrlWhere to ask.
hookSecretSigns the request, like a webhook.
hookTimeoutMs200–3000. Past that, the fallback applies.
hookFallbackstatic · reject · voicemail — what to do when your server is slow or wrong.

Choose the fallback deliberately. static is forgiving; reject is honest; nothing is worse than a caller hearing silence because a hook timed out.

Outbound ​

Two endpoints originate a call, and they are not the same thing:

EndpointUse it for
POST /api/v1/callsThe normal one. Assistant-driven, admission-checked, idempotent, produces a full call record. snake_case.
POST /api/v1/telephony/callsOriginating through a telephony account directly, for PBX-style dialling that is not an assistant call.
bash
curl -X POST "$CALLAGENT_API_URL/calls" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "01JQ…",
    "phone_number_id": "01JQ…",
    "to": "+40712345678",
    "idempotency_key": "order-4471-callback",
    "answering_machine_detection": true,
    "ring_timeout_seconds": 30
  }'

Full body: to (required), from, assistant_id, assistant_version_id, assistant_overrides, squad_id, phone_number_id, sip_trunk_id, customer_id, campaign_id, transport (pstn · sip · web · mobile), provider, variables, metadata, idempotency_key, answering_machine_detection, ring_timeout_seconds (5–300).

Admission ​

Before a leg exists, in this order:

  1. Destination policy — organization active and not frozen, direction enabled, allowlist, blocklist, country policy, number class (premium 1900/1976, satellite 870–874 and 881–883, North Korea 850, and the rest of the high-cost prefix tables), hourly, daily and daily-minutes caps.
  2. Routing — an outbound route must match; a blocked route throws route_blocked.
  3. Caller ID — a caller-ID policy pool, or the number you asked for only if your organization owns it; otherwise unverified_caller_id (403).
  4. Concurrency — included_concurrency + burst_concurrency, plus per-campaign, per-number and per-trunk dimensions. A denial is concurrency_limit (429).
  5. Originate — and if the carrier throws, the concurrency slot is released immediately, because nothing will ever send a hangup for a call that never began.

Separately, billing admission runs cheapest-check-first: organization status, trial expiry and phone verification, dunning freeze, daily spend cap, plan volume ceilings, funds, concurrency. It does not debit an estimated cost up front; usage is metered continuously.

Refusals arrive as an ended_reason on the call, not as an opaque 500:

ended_reasonCaused by
concurrency_limitConcurrency denial.
destination_blockedRoute blocked, blocklisted, country blocked, prefix blocked, premium, satellite, high cost, any cap.
telephony_errorMissing destination, no route, missing from, unverified caller ID, unknown provider, not implemented, missing credential, connection error.
provider_errorAnything else the carrier said.

Idempotency ​

A key is generated for you if you omit one. With a key, a repeat returns the existing call row and does not dial again — enforced by a unique index on (organization_id, idempotency_key), not by application logic alone.

One caveat: the replay is not compared against the original request. Reusing a key with a different body returns the original call rather than an error.

Routes ​

GET/POST     /api/v1/telephony/inbound-routes
PATCH/DELETE /api/v1/telephony/inbound-routes/{id}
GET/POST     /api/v1/telephony/outbound-routes
PATCH/DELETE /api/v1/telephony/outbound-routes/{id}

Inbound routes match a number and win over the number's own destination. Outbound routes pick the trunk or account a dialled number leaves through, and a route can be blocked to refuse a destination outright.

Transfers ​

Eight transfer modes exist: blind, warm, assistant_warm, dynamic, sip, phone, queue, on_hold_specialist. warm, assistant_warm and on_hold_specialist are consultative.

The contract spells these differently

TransferPlanSchema in @callagent/contracts uses blind, warm, warm_with_summary, warm_with_assistant, hold_specialist. The PHP enum that drives the transfer service uses the eight above. They do not map one to one. Use the PHP spelling over the REST API.

Warm transfer is implemented with FreeSWITCH commands (hold, originate, park, bridge) and has no Twilio equivalent. On a Twilio call, a "warm" transfer is a conference join, and a private consultation between two operators is not offered rather than faked. See Contact centre.

API ​

GET/POST         /api/v1/telephony/phone-numbers
POST             /api/v1/telephony/phone-numbers/search
POST             /api/v1/telephony/phone-numbers/purchase
POST             /api/v1/telephony/phone-numbers/import
GET/PATCH/DELETE /api/v1/telephony/phone-numbers/{id}
POST             /api/v1/telephony/phone-numbers/{id}/configure
POST             /api/v1/telephony/phone-numbers/{id}/release

GET/POST         /api/v1/telephony/accounts
GET/PATCH/DELETE /api/v1/telephony/accounts/{id}
POST             /api/v1/telephony/accounts/{id}/health
GET              /api/v1/telephony/providers

GET/POST         /api/v1/telephony/inbound-routes
PATCH/DELETE     /api/v1/telephony/inbound-routes/{id}
GET/POST         /api/v1/telephony/outbound-routes
PATCH/DELETE     /api/v1/telephony/outbound-routes/{id}

POST             /api/v1/telephony/calls

Full parameters and responses: phone numbers, accounts, routes, outbound calls.

Documented against the code. Anything not yet implemented is labelled, never described as working.