Skip to content

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.

Call recording is controlled by a single org-level flag in organization_twilio_settings:

call_recording_enabled BOOLEAN -- default: false

Toggle 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.

Call events use Twilio Event Streams, managed independently from SMS events. Each type gets its own sink and subscription.

-- organization_twilio_settings
sms_subscription_sid TEXT -- Twilio subscription for SMS events
call_subscription_sid TEXT -- Twilio subscription for call events
sms_events_active BOOLEAN -- SMS event streaming enabled
call_events_active BOOLEAN -- Call event streaming enabled

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-sink
Body: { "sms_events": bool, "call_events": bool }
POST /api/twilio/remove-event-sink
Body: { "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 TypePurpose
com.twilio.voice.status.answeredTriggers REST API recording start
com.twilio.voice.status.completedUpdates call status
com.twilio.voice.status.recording.processedTriggers recording download
com.twilio.voice.insights.call-summary.completeCaptures trunk/SIP calls

The operation is idempotent — existing sinks pointing to the same URL are reused.

Event sink setup requires TwilioCollectionPermission::Create. Teardown requires TwilioCollectionPermission::Delete.

All call events arrive at POST /twilio/events. The handler routes by event type:

  1. Extracts CallSid, From, To from request.parameters (PascalCase keys)
  2. Resolves phone number and call direction via resolve_phone_and_direction
  3. Creates a call record in the database
  4. If call_recording_enabled is true, starts recording via start_call_recording_util
  1. Looks up existing call record by call_sid
  2. Updates status to completed with duration
  3. Gracefully skips unknown calls (trunk calls may not have a record yet)

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.

  1. Checks RecordingStatus — skips non-completed recordings (Twilio fires both in-progress and completed)
  2. Checks idempotency — skips if call.recording_url is already set
  3. If no call record exists (trunk call), creates one by fetching call details from Twilio REST API
  4. Downloads recording MP3 to S3
  5. Sets recording_url in the database immediately after upload (before transcription) to prevent race conditions
  6. Transcribes audio via OpenAI Whisper
  7. Deletes recording from Twilio after successful upload + transcription

Calls routed through SIP trunks or Twilio Studio bypass Loquent’s TwiML entirely. These calls are captured through two mechanisms:

  1. Voice Insights call-summary.complete events fire for all calls on the account
  2. Recording webhooks create call records on-the-fly when no prior event exists, using fetch_twilio_call_util to 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.

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 transcription

Recording deletion from Twilio is best-effort — a failed deletion logs a warning but does not fail the pipeline.

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.

MethodPathDescription
POST/api/twilio/setup-event-sinkCreate event sink + subscription (SMS and/or call)
POST/api/twilio/remove-event-sinkTear down event subscription
PathPurpose
POST /twilio/eventsReceives all Event Streams events (SMS + call)
POST /twilio/call-statusCall status callback from TwiML action URL
POST /twilio/recordingRecording status callback
FunctionFilePurpose
start_call_recording_utilutils/start_call_recording_util.rsStart recording on an active call via REST API
fetch_twilio_call_utilutils/fetch_twilio_call_util.rsFetch call details from Twilio REST API
list_call_recordings_utilutils/list_call_recordings_util.rsList recordings for a call
delete_event_subscription_utilutils/delete_event_subscription_util.rsTear down a Twilio event subscription
update_event_subscription_utilutils/update_event_subscription_util.rsUpdate subscription event types
resolve_phone_and_directionutils/resolve_phone_direction_util.rsBatch phone lookup + direction resolution
is_recording_enabledutils/is_recording_enabled_util.rsFail-closed recording check
-- organization_twilio_settings (new columns)
call_recording_enabled BOOLEAN DEFAULT false
call_events_active BOOLEAN DEFAULT false
sms_subscription_sid TEXT -- replaces event_subscription_sid
call_subscription_sid TEXT -- replaces event_subscription_sid
-- phone_number (new constraint)
CREATE UNIQUE INDEX idx_phone_number_unique ON phone_number (number);