Call Recording
Loquent supports configurable call recording for BYO Twilio accounts. Organizations toggle recording on or off, and the system handles TwiML generation, recording capture, transcription, and cleanup automatically.
Recording Toggle
Section titled “Recording Toggle”Call recording is controlled by a single org-level flag in organization_twilio_settings:
call_recording_enabled BOOLEAN -- default: falseToggle it from Settings → Twilio → Telephony → Configuration. The setting takes effect immediately — no restart or redeployment required.
When enabled, Loquent records calls through two mechanisms depending on how the call is routed:
- Loquent-managed calls — TwiML includes a
<Record>block - REST API recording — for calls answered via Event Streams, recording starts via Twilio’s REST API
When disabled, calls complete normally without recording. The TwiML still includes a status callback (action URL) so call status updates work regardless of the recording setting.
Event Streaming
Section titled “Event Streaming”Call events use Twilio Event Streams, managed independently from SMS events. Each type gets its own sink and subscription.
Database columns
Section titled “Database columns”-- organization_twilio_settingssms_subscription_sid TEXT -- Twilio subscription for SMS eventscall_subscription_sid TEXT -- Twilio subscription for call eventssms_events_active BOOLEAN -- SMS event streaming enabledcall_events_active BOOLEAN -- Call event streaming enabledSetup and teardown
Section titled “Setup and teardown”Set up call event streaming from Settings → Twilio → Call Event Streaming using the Setup button. Remove it with the Remove button. Each operates independently of SMS event streaming.
POST /api/twilio/setup-event-sinkBody: { "sms_events": bool, "call_events": bool }POST /api/twilio/remove-event-sinkBody: { "event_type": "sms" | "call" }Setup creates a Twilio Event Sink pointing to https://{APP_HOST}/twilio/events and a subscription for these call event types:
| Event Type | Purpose |
|---|---|
com.twilio.voice.status.answered | Triggers REST API recording start |
com.twilio.voice.status.completed | Updates call status |
com.twilio.voice.status.recording.processed | Triggers recording download |
com.twilio.voice.insights.call-summary.complete | Captures trunk/SIP calls |
The operation is idempotent — existing sinks pointing to the same URL are reused.
Permissions
Section titled “Permissions”Event sink setup requires TwilioCollectionPermission::Create. Teardown requires TwilioCollectionPermission::Delete.
Call Event Processing
Section titled “Call Event Processing”All call events arrive at POST /twilio/events. The handler routes by event type:
call.answered
Section titled “call.answered”- Extracts
CallSid,From,Tofromrequest.parameters(PascalCase keys) - Resolves phone number and call direction via
resolve_phone_and_direction - Creates a
callrecord in the database - If
call_recording_enabledis true, starts recording viastart_call_recording_util
call.completed
Section titled “call.completed”- Looks up existing call record by
call_sid - Updates status to
completedwith duration - Gracefully skips unknown calls (trunk calls may not have a record yet)
call-summary.complete (Voice Insights)
Section titled “call-summary.complete (Voice Insights)”Fires for all calls on the account, including SIP trunk and Twilio Studio calls that bypass Loquent’s TwiML. Creates call records for calls that have no prior event in the system.
recording.processed
Section titled “recording.processed”- Checks
RecordingStatus— skips non-completedrecordings (Twilio fires bothin-progressandcompleted) - Checks idempotency — skips if
call.recording_urlis already set - If no call record exists (trunk call), creates one by fetching call details from Twilio REST API
- Downloads recording MP3 to S3
- Sets
recording_urlin the database immediately after upload (before transcription) to prevent race conditions - Transcribes audio via OpenAI Whisper
- Deletes recording from Twilio after successful upload + transcription
Trunk Call Support
Section titled “Trunk Call Support”Calls routed through SIP trunks or Twilio Studio bypass Loquent’s TwiML entirely. These calls are captured through two mechanisms:
- Voice Insights
call-summary.completeevents fire for all calls on the account - Recording webhooks create call records on-the-fly when no prior event exists, using
fetch_twilio_call_utilto get call details from the REST API
The resolve_phone_and_direction helper handles phone lookup and direction resolution with a single batched query for both from and to numbers.
Recording Lifecycle
Section titled “Recording Lifecycle”Call answered → (if recording enabled) Start recording via REST API → Recording completes → Twilio fires recording.processed → Download MP3 → Upload to S3 → Set recording_url in DB (idempotency marker) → Transcribe with OpenAI Whisper → Delete recording from Twilio → Update call record with transcriptionRecording deletion from Twilio is best-effort — a failed deletion logs a warning but does not fail the pipeline.
Safety: Fail-Closed Recording
Section titled “Safety: Fail-Closed Recording”is_recording_enabled() uses fail-closed logic. If any of these lookups fail, recording defaults to off:
- Phone number not found in database
- Organization Twilio settings not found
- Database connection error
This is intentional — unexpected recording is worse than a missed recording.
API Reference
Section titled “API Reference”| Method | Path | Description |
|---|---|---|
POST | /api/twilio/setup-event-sink | Create event sink + subscription (SMS and/or call) |
POST | /api/twilio/remove-event-sink | Tear down event subscription |
Webhook Endpoints (Twilio → Loquent)
Section titled “Webhook Endpoints (Twilio → Loquent)”| Path | Purpose |
|---|---|
POST /twilio/events | Receives all Event Streams events (SMS + call) |
POST /twilio/call-status | Call status callback from TwiML action URL |
POST /twilio/recording | Recording status callback |
Twilio Utility Functions
Section titled “Twilio Utility Functions”| Function | File | Purpose |
|---|---|---|
start_call_recording_util | utils/start_call_recording_util.rs | Start recording on an active call via REST API |
fetch_twilio_call_util | utils/fetch_twilio_call_util.rs | Fetch call details from Twilio REST API |
list_call_recordings_util | utils/list_call_recordings_util.rs | List recordings for a call |
delete_event_subscription_util | utils/delete_event_subscription_util.rs | Tear down a Twilio event subscription |
update_event_subscription_util | utils/update_event_subscription_util.rs | Update subscription event types |
resolve_phone_and_direction | utils/resolve_phone_direction_util.rs | Batch phone lookup + direction resolution |
is_recording_enabled | utils/is_recording_enabled_util.rs | Fail-closed recording check |
Database Changes
Section titled “Database Changes”-- organization_twilio_settings (new columns)call_recording_enabled BOOLEAN DEFAULT falsecall_events_active BOOLEAN DEFAULT falsesms_subscription_sid TEXT -- replaces event_subscription_sidcall_subscription_sid TEXT -- replaces event_subscription_sid
-- phone_number (new constraint)CREATE UNIQUE INDEX idx_phone_number_unique ON phone_number (number);