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 byfriendly_namewith the Twilioconf_sidattached.CallConfCalleeData(:28) is one leg — one agent or one external party.POST /api/v1/call/add_calleeadds a leg to a live conference; the frontend calls it fromTwilioClient.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.