Skip to main content

Telephony

Connecting a provider, mapping human teammates to phone numbers, and the callback endpoints the provider itself calls.

Providers​

Plivo and Exotel are supported. Plivo is the fuller integration — outbound campaigns, recording, and transfer all use Plivo's REST API.

List connected providers​

GET /api/voice/integrations/telephony

{
"providers": [
{
"provider": "plivo",
"is_connected": true,
"connected_at": "2026-09-02T10:11:12Z",
"phone_number": "+915550010000",
"auth_id": "MAXXXXXXXXXXXXXXXXXX"
}
]
}

Only non-secret fields are returned. Auth tokens never leave the database, even for an admin — the console shows whether a provider is connected, not what it is connected with.

Connect a provider​

PUT /api/voice/integrations/telephony/{provider} — admin only

{provider} must be plivo or exotel.

{
"auth_id": "MAXXXXXXXXXXXXXXXXXX",
"auth_token": "your-plivo-auth-token",
"phone_number": "+915550010000"
}
{ "provider": "plivo", "is_connected": true }

The body is stored as given, so provider-specific fields can be added without an API change. phone_number must be valid E.164 — a campaign aborts at startup if it is not.

Re-sending replaces the stored credentials.

Disconnect​

DELETE /api/voice/integrations/telephony/{provider} — admin only

Human agents​

Mapping teammates to phone numbers, so calls can be placed from — and handed to — a specific person.

Phone numbers​

GET /api/voice/agents/phone-numbers

{ "phone_numbers": [] }

PUT /api/voice/agents/phone-numbers/{user_uuid} — admin only

{ "phone_number": "+919876543210" }

DELETE /api/voice/agents/phone-numbers/{user_uuid} — admin only

This number is the leg that rings first on a click-to-call: the teammate answers, then the customer is dialled.

Tag assignments​

GET /api/voice/agents/tag-assignments

{ "tag_assignments": [] }

GET /api/voice/agents/tag-assignments/me — the caller's own

PUT /api/voice/agents/tag-assignments/{user_uuid} — admin only

{ "tags": ["north", "winback"] }

DELETE /api/voice/agents/tag-assignments/{user_uuid} — admin only

Tag assignments do double duty: they route handoffs to the right person, and they restrict what that person can see. A teammate assigned north sees only customers carrying that tag, and only calls belonging to those customers. A teammate with no assignment sees everything.

Click-to-call​

POST /api/voice/customers/{uuid}/calls

Dials the customer from the caller's own mapped number. This is a human call — no AI agent is involved — but it is still recorded and transcribed, and it appears in the call list with the teammate's name.

Outcomes specific to these calls: agent_answered, agent_no_answer, customer_no_answer, cancelled.


Provider callbacks​

These are called by the provider, not by you. They are unauthenticated by necessity and act only on calls that already exist in your database.

EndpointPurpose
ANY /api/voice/webhooks/plivo/answerInbound answer — returns the stream XML
ANY /api/voice/webhooks/plivo/answer/inboundSame handler, legacy path
ANY /api/voice/webhooks/plivo/answer/outboundCampaign answer
ANY /api/voice/webhooks/plivo/hangupRecords how a call ended
ANY /api/voice/webhooks/plivo/statusTerminal call state
ANY /api/voice/webhooks/plivo/recordingWhere the recording landed
ANY /api/voice/webhooks/plivo/handoffReturns the transfer XML
ANY /api/voice/webhooks/plivo/stream-statusThe provider's verdict on the media stream
ANY /api/voice/webhooks/plivo/click-to-call/answer/:call_uuidHuman call, agent leg answered
ANY /api/voice/webhooks/plivo/click-to-call/status/:call_uuidHuman call, final state
ANY /api/voice/webhooks/exotel/answerInbound answer
ANY /api/voice/webhooks/exotel/statusCall status

Both GET and POST are accepted because providers differ and some retry with the other method.

Pointing a provider at them​

Configure the Plivo application with:

Answer URL : https://your-host/api/voice/webhooks/plivo/answer   (POST)
Hangup URL : https://your-host/api/voice/webhooks/plivo/hangup (POST)

Outbound campaign calls set their own answer and hangup URLs at dial time; you do not configure those.

Why an inbound call might not be answered​

The answer webhook looks up an active inbound flow whose number matches the one dialled. No match returns hangup XML rather than answering in the wrong company's voice:

[plivo] no active inbound flow for +915550010000 — hanging up

Check that the flow is active, not draft, and that its phone_number matches on its last ten digits.

The media socket address​

The WebSocket URL handed to the provider is derived from the request's own Host header, not from PUBLIC_BASE_URL — the provider reached you on that host, so it is reachable by definition. A configured base URL can be stale after an ngrok restart, or carry stray whitespace that makes the provider reject the whole XML document as invalid, which looks like a working call with no transcript.

PUBLIC_BASE_URL remains the fallback, and is what outbound campaigns use to build their callback URLs — which is why a campaign refuses to start without it.