Audio & Video Calls
Audience: Sales · Support · QA · Management · Engineering · Where in app: Calls (recordings, AI calls, phone transcripts) · configured under Settings → Calls/Preferences and Settings → AI → Agents · Plan availability: All plans (AI voice, inbound, and outbound phone may be plan- or add-on-gated — verify)
Knock Knock supports several flavors of voice (and video) calling that share infrastructure but use different providers under the hood. A visitor can talk to a human over Twilio WebRTC in the browser, talk to an AI voice agent (ElevenLabs over LiveKit WebRTC, or Ultravox), or call/be-called over the regular phone network (PSTN — carriers Twilio and Telnyx) where an AI agent answers via an ElevenLabs media-stream bridge. Humans can take over an AI call mid-conversation or silently monitor it for QA. Everything is logged, most things are recorded or transcribed. Carrier/number/wallet details: Telephony subsystem.
What it does
- Browser calls (WebRTC): audio or video between a visitor and a human agent, via Twilio Video. Always recorded.
- AI voice calls: the AI answers the visitor by voice (ElevenLabs signed-URL or Ultravox), can book meetings and look up products mid-call.
- Inbound phone calls: a visitor dials the company's number; an AI agent answers over PSTN through the ElevenLabs bridge.
- Outbound phone calls: the company (via ms-ai) dials a lead; the same AI bridge runs the conversation.
- Takeover & monitor: a human can jump into an in-progress AI call (takeover) or listen silently (monitor) for QA.
- Captures recordings (WebRTC) and turn-by-turn transcripts (AI calls) and surfaces them in Calls.
How it works
The three flavors at a glance
| Flavor | Provider | Triggered when |
|---|---|---|
| Human ↔ Visitor (audio/video) | Twilio Video (WebRTC) | call_routing = human and an agent is online |
| AI ↔ Visitor (audio) | ElevenLabs Conversational AI over LiveKit WebRTC (calling_type: 'webrtc') or Ultravox | call_routing = ai, or fallback when no human |
| Inbound / Outbound phone | Twilio or Telnyx PSTN + ElevenLabs media-stream bridge | Visitor dials the company number (inbound) / ms-ai dials a lead (outbound) |
Twilio WebRTC lifecycle (human ↔ visitor)
- Visitor clicks call. If
take_phone_number_before_callis set, a phone number is captured first (stored in the widget'suser_profile); contact-info questions can also run. - The widget creates a service room (
POST /service-room/create) and emitstwilio-webrtc-call. The server (handleTwilio) mints Twilio tokens and creates a Twilio Video room named after theservice_room_id, withrecordParticipantsOnConnect: true. - The widget retries
new-call-requestevery ~5s; available agents get an "Incoming call". The desktop app pops a dedicated call window and rings (VoIP push). - First agent to accept (
call-accepted-by-agent) joins the Twilio room; the room is markedis_started: trueand Firestore status flips toaccepted. Call begins. - On hangup, the call event is saved and the live session updated (
update-live-session). - Recordings are participant recordings; the merged video is produced asynchronously (see ms-sessions,
RoomVideoMergingjob).
Ringtones in the widget: before-phone.mp3 (connecting, loops) → on-call-ringing.mp3 (agent available, loops) → drop-call.mp3 / end-call.mp3 (one-shot on end/decline). Total ring window is ~35s before falling back.
AI voice lifecycle (ElevenLabs / Ultravox, in-browser)
- Visitor clicks call; contact info captured as available. A ringback loop (
on-call-ringing.mp3, started inside the click gesture inwidget/src/stores/widget.ts) plays until the agent picks up. - ElevenLabs path (WebRTC / LiveKit): widget emits
get-singed-url; ms-communication (CallsController,calling_type: 'webrtc'hardcoded) calls ms-aistart-audio-calland returnselab-signed-urlcarrying a LiveKit conversationtoken+connection_typeandprevious_conversationmemory (widget waits 30s, was 15s). The widget starts the ElevenLabs SDK with{ conversationToken, connectionType: 'webrtc' }; the legacy websocket transport (signedUrl) still works whenconnection_type: 'websocket'. Dynamic variables (company_id,user_session_id,webhook_url, timezones, calendar type) and client tools (askUserDetails,saveUserProfile,esclateToHuman— legacy spelling on this path) pass through unchanged; transcripts/typed messages/tool responses arrive via SDK callbacks instead of the raw socket. - Ultravox path: widget emits
connect-call-with-ai-agent/get-audio-call-token; receivesai-agent-joining-data(20s timeout) and joins via the Ultravox client SDK. - Tool calls (book a meeting, look up a product) route through ms-ai's voice tools. Product results come back over
events-gateway(ElevenLabs) /voice-ai-tools-gateway(Ultravox) and the widget displays matched products. Booking confirmation is delivered to the agent as auserMsg(on LiveKit there is no raw socket to inject context). - On end, the widget emits
ai-all-messages(full transcript, collected name/email/phone, booking detail). The transcript is captured for the call record.
Max AI call duration is 900s (15 min). Inactivity prompts fire at ~10s / 15s / 20s, after which the AI ends the call gracefully.
Inbound phone call lifecycle (PSTN → AI)
- The number's carrier webhook hits
POST /twilio/inbound-voice/:company_id(Twilio) orPOST /telnyx/inbound-voice/:company_id(Telnyx TeXML,TelnyxInboundCallController) withagent_id/agent_key. - Wallet gate: for app-bought numbers, ms-comm asks ms-ai
GET /elevenlabs/telephony-inbound-allowed/:company_id?number=(4s timeout, fail-open); wallet-empty callers hear "This number is temporarily unavailable. Goodbye." BYO numbers are never gated. - Routing priorities: if the agent has
routing_priorities, ms-comm rings those numbers in order (each with its ownring_timeout, chain advanced viaPOST /twilio/inbound-dial-fallback/:company_id) regardless of working hours; the first to answer takes the call, exhausted/error falls through to the AI. - If inbound working hours are configured and it's within hours, the call is transferred to the human
transfer_numberinstead of the AI. - Otherwise the bridge "preheats" (signed URL, agent config, company context, visitor context) and returns TwiML/TeXML (
<Connect><Stream>) pointing at the bridge WebSocket (/twilio/media-streamor/telnyx/media-stream— the path selects the carrier codec handling). ElevenLabsBridgeServiceopens two WebSockets — carrier (mu-law 8kHz; Telnyx A-law legs are normalized byAudioTranscodeHelper) and ElevenLabs (PCM 16kHz) — transcodes audio both ways, and fans the audio out to theai-listen:{service_room_id}room for supervisors (carrier-agnostic).- Turn-by-turn transcript is appended to
InboundOutboundCallModel.transcripton everyuser_transcript/agent_response. On completion the record is finalized; the transcript email and post-call webhook follow (see Post-call below).
Inbound config lives on companies.ai_inbound_call (single agent) or companies.ai_inbound_call_agents[] (multi-agent, keyed by agent_key): agent_id, agent_key, name, transfer_number, working_hours, post_call_webhook_url / _header. Caller-ID identity verification fires an identity-graph lookup (/identity-graph/markdown-by-phone, /identity-graph/inbound-call-verify) so the AI greets a known caller with context.
Outbound phone call lifecycle (AI → lead)
- ms-ai places the call (ms-communication does not dial directly); the number's
providerpicks the carrier (Twilio REST or Telnyx TeXML create-call). Before dialing, ms-ai hitsPOST /twilio/preheat-outbound/:company_id/:agent_id(or/telnyx/preheat-outbound/...) to warm caches. - When the customer answers, the carrier hits
POST /twilio/outbound-voice/:company_id/POST /telnyx/outbound-voice/:company_id, returning the same media-stream bridge TwiML/TeXML (purpose: 'outbound'). - Same transcoding/transcript/fanout as inbound; on completion the agent's
post_call_webhook_urlreceives the full call metadata + transcript (see below).
Post-call pipeline
The post-call webhook is held until the ElevenLabs analysis arrives (it carries summary, sentiment, recording), single-fired via an atomic claim on post_call_webhook_sent_at. Backend PostCallWebhookFallbackJob (every 1 min, 5-min grace, 24h max age, 5 attempts) fires calls whose analysis never lands — the claim guarantees exactly-once. Payload (PostCallWebhookService, namespaced next_level_call): agent_key, transcript[], transcript_text, summary, call_sentiment (positive|neutral|negative, classified from the caller's words via ms-ai POST /agents/classify-call-sentiment), signed recording_url (HMAC, ~365d, RECORDING_LINK_SECRET, fail-closed, served by backend GET /public/recordings/:id on api.nextlevelai.site). Outbound voicemail detection (analysis flag OR transcript heuristic → record voicemail) and call_outcome (booked|qualified|voicemail|no_answer|no_outcome, upgrade-only) drive GHL outcome tagging via backend /internal/ai-call/contact-extracted. Full detail: Telephony subsystem.
Outbound config: companies.ai_outbound_call_enabled (feature on), ai_outbound_call_allowed (admin gate / allowed initiators), ai_outbound_call_agents[] (per-agent: agent_key, name, webhook). Knox can place calls via its knox-place-call tool when granted.
Configuration & options
Routing modes (companies.ai_config.call_routing)
| Mode | Behavior |
|---|---|
ai | AI voice handles every call |
human | Calls go to humans; if none online, fall back to missed-call / offline message |
ai_to_human | AI starts; takeover transfers to a human when triggered. The escalation tool is only offered when AGENTS_STATUS == 'AVAILABLE' — named escalateToHuman on the PSTN bridge, legacy esclateToHuman in the widget path. See Telephony › Human escalation for creds/caller-ID behavior |
Provider & call settings
| Setting | Purpose |
|---|---|
ai_config.call_model_provider | Selects the AI tech (elevenlabs, fixie-ai/ultravox, openai-realtime) — verify exact field/values per tenant; some live in ms-ai/company schema |
take_phone_number_before_call | Capture the visitor's phone before starting a call |
allow_twilio_calls | Enable phone-call fallback |
always_twilio_calls | Force the Twilio path even when AI voice is available (compliance/recording) |
company_phone_number | Number used for phone fallback |
ai_inbound_call.* / ai_inbound_call_agents[] | Inbound AI agent(s), transfer number, working hours, post-call webhook |
ai_outbound_call_enabled / ai_outbound_call_allowed / ai_outbound_call_agents[] | Outbound calling gates and agents |
communication_options.take_over_permissions | Whether agents may take over AI calls |
Calls dashboard tabs
The Calls screen (CallsView.vue) has: Call Recordings (WebRTC video/audio, heard/unheard, play/view-session/delete), AI Calls (in-browser AI voice + transcript), Forwarded Calls (phone-fallback recordings), and Inbound/Outbound Calls (AI phone transcripts with direction, caller, duration, status). Live phone calls show a LIVE badge and a Listen button (opens the live-call listener); completed ones show Play. Unheard badges and "Mark all heard" are per-tab; the list polls every ~10s while any live call exists.
Behaviors & edge cases
- Takeover (human joins an AI call). Socket
ai-call-takeover-requestends the AI service room (ended_by: 'agent_takeover'), clearsis_ai_call, creates a new service room (takeover_from_ai: true) with fresh Twilio tokens, and tells the widget (ai-call-takeover, auto-accepts) and the dashboard (twilio-webrtc-call-response). Gated bytake_over_permissions. Inbound PSTN has its owntakeover_inbound_callpath where the agent's mic audio is injected into the live Twilio call via the bridge's pub/sub. - Monitor (silent listen for QA). Socket
monitor_call(browser AI) /monitor_inbound_call(PSTN) joins theai-listen:{service_room_id}room — carrier-agnostic (Twilio and Telnyx bridges feed the same room). Access is verified: the session's call must be active and AI, and the agent must belong to the company. The widget streams PCMaudio_chunks (base64) only while a listener is present (request_audio_stream/stop_audio_stream); on WebRTC calls the widget taps the agent's LiveKit audio track to build that stream. Listener counts and streaming flags are tracked in Redis (~15-min TTL). The monitor confirms streaming within ~5s. - Permission fallbacks (WebRTC). Mic granted + camera denied → audio-only with
cameraDenied. Mic denied → listener mode (micDenied): the visitor can see/hear the agent but cannot broadcast; they can publish tracks later. iOS Safari background suspension is detected (track ended) and the widget attempts to restart the video track. - Missed call. If a user-initiated call isn't answered before the ring window, it's logged (
loki.logTimeout) and a missed-call email is sent (sendMissedCallEmail). A separate AI voice missed-call email fires when an inbound AI call ends with no action (e.g. no booking). - Recording. WebRTC: always on (
recordParticipantsOnConnect: true), merged async by ms-sessions. AI in-browser: transcript captured (audio recording optional via ElevenLabs). PSTN: the bridge does not record; audio streams on demand from ElevenLabs via signed links (RecordingLinkService, HMAC ~365d, fail-closed) — never stored.always_twilio_callsforces the recorded Twilio path. - Stale-call sweep. In-progress phone calls with no update past a ~5-min cutoff are swept to
failedso the dashboard doesn't show phantom live calls. - Timeouts (engineering reference). Max session duration is config-driven; signed-URL / WebRTC-token cache ~45s (widget waits 30s for
elab-signed-url); agent-config cache ~10min; company-context cache ~2min; visitor-context init cap ~1.2s (inbound prewarm cap ~11s); transfer dial timeout 30s; routing-priority ring timeouts 5–120s (default 20s); AI max duration 900s. - Call rate limit: 3 points / 60s per connection (much stricter than chat) to prevent call spam.
Plan & limits
- Browser WebRTC human calls and basic AI voice are documented as available on all plans.
- Inbound/outbound AI phone calling, provisioned phone numbers, and some recording/compliance features are likely add-on or higher-tier — verify against the current plan matrix before quoting.
- Hard limits in code: AI calls cap at 900s; ring window ~35s; transfer dial 30s. There is no documented per-tenant monthly call cap here (billing/credits govern AI minutes) — verify.
Technical implementation
- Owning service: ms-communication.
app/controllers/CallsController.js— Twilio WebRTC, AI-call setup (WebRTC token / signed URL / Ultravox), takeover, monitor, missed-call email.app/controllers/InboundCallController.js,OutboundCallController.js,InboundOutboundCallController.js— Twilio PSTN webhooks, routing priorities, wallet gate, preheat, transcript persistence, sentiment/voicemail/outcome classification.app/controllers/TelnyxInboundCallController.js,TelnyxOutboundCallController.js+app/helpers/TelnyxHelper.js,AudioTranscodeHelper.js— Telnyx TeXML surface and A-law transcoding.app/services/ElevenLabsBridgeService.js— the dual-WebSocket media-stream bridge (transcode, tool calls incl.escalateToHuman, transcript, takeover/monitor fanout).app/services/PostCallWebhookService.js,RecordingLinkService.js— post-call webhook payload + signed recording links (backend holds the twin copy + fallback jobPostCallWebhookFallbackJob).
- Widget: widget —
src/services/call.ts,twilio.ts(Twilio Video/Client SDK, listener mode, screen share, camera switch),aiCall.ts(ElevenLabs/Ultravox sessions, live-listen audio capture),sound.ts,socket.ts. - Agent UI: frontend —
src/views/CallsView.vue,ChatsView.vue(one-click call from a chat). - Recording merge: ms-sessions —
RoomVideoMergingjob; see Session Recording. - AI voice tools / transcripts: ms-ai; Identity Graph (caller-ID verification,
visitor_context).
Related
- Telephony subsystem — carriers, number lifecycle, wallet, escalation
- Live Chat — escalate a chat into a call
- Auto-Triggers — auto-connect video popups that start a call
- Notifications — incoming-call VoIP push, missed-call and AI-voice missed-call emails
- Session Recording
- ms-communication service