Skip to content

Telephony

Everything voice-related runs on Twilio. This page describes which Twilio products the system uses, how an inbound call is routed, and where the code for each step lives.

Twilio products in use

Product Used for
Programmable Voice + TwiML Call control: Dial, Gather, Conference, Enqueue, Say, Record
Voice SDK access tokens Browser and mobile clients, issued at GET /api/v1/calls/twilio/token (voip/calls/urls_api.py:10)
Elastic SIP (domains + credential lists) Hardphones and third-party softphones
Queues Holding callers when no agent is free
Messaging SMS send, receive and status
Push credentials (client.chat.credentials) FCM and APNs tokens for Voice SDK push
Pricing and number provisioning Buying DIDs from the settings UI
Usage Records Billing reconciliation into voip/twilio_charges

TaskRouter is not used. Routing is hand-rolled from Queues, Conferences and the voip/redirects rule engine. Do not assume TaskRouter semantics when reading the code.

Calls are conferences, not dials

The single most important implementation fact: an answered call is bridged into a Twilio conference, not a point-to-point Dial. That is what makes transfer, barge-in, adding a second callee and hold possible.

  • CallConfData (voip/call_trace/models/call_conf_data.py:7) is the conference identity, keyed by friendly_name with the Twilio conf_sid attached.
  • CallConfCalleeData (:28) is one leg — one agent or one external party.
  • POST /api/v1/call/add_callee adds a leg to a live conference; the frontend calls it from TwilioClient.addCallee() (frontend/src/lib/actions/twilio/TwilioClient.ts:181-209).

Inbound call flow

The webhook names read almost like a flow chart. They are declared in backend/settings/twilio/endpoints.py:11-35 and wired in backend/voip/calls/urls_wh.py, all under /webhooks/twilio/call/.

inbound call to a company DID
        │
        ▼
  new ──────────► open / closed check against AttentionHours
        │                 │ closed
        │                 └──► new/msg  (closing message, voicemail)
        ▼
  new/lang  ──► language Gather (contact language or IVR choice)
        │
        ▼
  new/ext/dial ──► extension Gather ──► new/ext/call  (ring that extension)
        │
        ▼
  new/conf ──► create the conference, ring the eligible agents
        │
        ├── an agent answers ──► conf/join/<sid> ──► conf/in-out/<sid>
        │                                              │
        │                                              ▼
        │                                     conf/completed/<sid>
        │
        └── nobody free ──► Enqueue ──► dequeue (when an agent frees up)

  status, action/<sid>, error   — lifecycle callbacks on every branch
  record, voicemail             — media capture

Routing eligibility combines three things: AttentionHours for the company (voip/companyServices/models/attention_hours.py:10), the redirection rules in voip/redirects/models.py, and the live per-agent state in CallTrace (voip/call_trace/models/__init__.py:11), whose status is one of Occupated, Available, OpenCall or Record.

SMS has its own two endpoints: /webhooks/twilio/sms/receive and /webhooks/twilio/sms/status.

Two generations of webhooks coexist

/webhooks/** (from voip/urls_wh.py) is current. /twilio/hooks/** (from calls/urls.py) is the earlier generation — only token, record and voicemail remain, and most of the module is commented out (backend/calls/urls.py:11-21). New work belongs under /webhooks/.

Outbound calls

From the browser, TwilioClientWeb fetches an access token (GET /api/v1/calls/twilio/token), registers a Device, and dials with device.connect({ params: { To } }) (frontend/src/lib/actions/twilio/TwilioClientWeb.ts:202). The TwiML application identified by TWILIO_TWIML_APP_SID points back at the webhook that places the outbound leg and joins it to a conference.

From a mobile device the same flow runs through the native CallPlugin, so that the OS call UI, ringtone and audio session behave like a real phone call. See Frontend → The softphone.

Webhook signature verification

Twilio requests are verified by IsTwilio (backend/settings/twilio/permissions/is_twilio.py:9), which checks X-Twilio-Signature against several candidate URLs so that a reverse proxy rewriting the host or scheme does not invalidate the signature. Because DRF's default permission in this project is AllowAny (see Backend → Authentication), a webhook view that does not declare IsTwilio is publicly callable. Check this per view.

Keeping webhook URLs in sync

TWILIO_WEB_HOOK_DOMAIN determines where Twilio sends callbacks. When it changes — a new staging host, a new tunnel — the Twilio-side configuration has to be rewritten; backend/tools/set_webhooks.py is the operator script that does it.

Billing reconciliation

Twilio usage is mirrored locally so that invoices can be checked: TwilioCallRecord, TwilioMessageRecord and TwilioRecordingRecord (voip/twilio_charges/models/) store raw Twilio data keyed by SID. update_all_twilio_data() pulls it, triggered by POST /google/hooks/charges. backend/tools/compare_charges.py and manual_twilio_charges_update.py are the manual counterparts.