A call is a room
Every conversation (web, outbound, inbound) is a session, and a session is a room the agent joins. Start with outbound, the busiest path: The agent joins before the dial, so a fast pickup never lands on a cold agent. The muted mic means ringback tone and carrier menus can’t confuse it. Inbound is one sentence: the customer calls your number and the agent answers. On web, your server mints a token and the browser joins with any LiveKit client. The agent joins as soon as the browser does. ThesessionId you get back is the key to everything later: status while live, transcript and outcomes after.
Inside a turn
The caller speaks, and three models run as a streaming pipeline. The numbers match the four legs in Where the time goes: Nothing waits for anything to finish. STT emits text as the caller talks. The LLM starts replying from the finalized turn, and the first sentence goes to TTS while the rest is still generating. Turn-taking is voice-activity based: the agent decides the caller is done from short silences, tuned for Urdu conversation rhythm. Underneath, we run on LiveKit, deployed in Pakistan. That’s what lets us call with Pakistani phone numbers. Our GPUs sit close by too, so end-to-end latency is about as low as you’ll get here.The edge cases we already handle
Real calls are messy. We handle the mess in-house, so you only think about the prompt, the conversation flow, and your tools:- Interruptions, the Urdu way. Listening noises like “جی جی” and “اچھا ٹھیک ہے” never stop the agent, no matter how often. A real “رکیں” or “نہیں” cuts it off immediately. A “ہیلو؟” mid-sentence doesn’t stop it. That caller wants to hear more, not less.
- Voicemail. On outbound we detect the answering machine, mark the call, and hang up. You don’t pay to talk to a beep.
- Silence handling. A silent caller gets one “are you still there?” in the conversation’s language, then a graceful hangup. Never while a tool is running.
- Phantom answers.
answeredAtis stamped on real human speech, not on the carrier saying the call connected. A pickup where nobody ever speaks is classifiedsilent_pickup, not a conversation. - Bad lines. If the caller audibly spoke but the transcription never caught a turn, the agent greets anyway instead of holding dead air.
- Dial failures come back classified (
wrong_number,busy,no_answer,unreachable), with the carrier’s SIP code preserved.
Ending the call
The agent can always end the call. It’s a built-in tool, nothing you configure. When it does, it records why, in the LLM’s own words:order confirmedcustomer not interestedwrong numbercustomer not available, will call back latercustomer self harm detected. This one really happened: in a farming-agent test the caller said they had drunk the chemical, and kept asking why they shouldn’t. The LLM caught it and ended the call.
endedBy: agent tells you the agent ended it.
When the agent calls your tools
Mid-conversation tool calls run while the caller waits. By default they wait in silence. Turn on background noise and ambient room tone covers the gap. A tool gets a 10-second budget. On timeout the agent gets an error and keeps talking, so a slow tool never drops the call. Every tool call, with arguments and result, is on the session detail afterwards.Where the time goes
Every turn has four legs:- STT: time to finalized text
- End of turn: deciding the caller is done
- LLM: time to first token
- TTS: time to first audio. Uplift AI TTS first byte is 100–200ms.
gemini-2.5-flash. It’s the fastest — see Provider & model reference. The thinking knobs trade intelligence for exactly this latency.
After the call ends
We upload the recording and a structured report, and the post-call pipeline runs: the transcript is assembled, the scorecard grades the call, conversions and callbacks are extracted. Only then do webhooks fire. So by the timecall.completed reaches you, everything it announces is already readable via the API.
Every dial attempt leaves a session record and runs this pipeline, connected or not (a no-answer just skips grading). The transcript appears seconds after hangup.
The whole life of a call, in the timestamps you’ll see in the JSON:
The gap between connectedAt and answeredAt is where phantom answers live. The gap between endedAt and call.completed is why an immediate read can be empty.
