Cosmo Realtime SDK
Telephony

Outbound calls

Start a session, then dial — the callee joins the agent's room as a SIP participant.

A phone call is not a different kind of session. You start a normal realtime session (usually server-side, with an API key), then dial: the platform places a SIP call, and when the callee answers they join the same LiveKit room the agent is already in. The agent neither knows nor cares that the voice arrived over the phone network.

import asyncio, os
from cosmo_ai import AudioConfig, CosmoRealtime, RealtimeTranscriptDelta

async def main() -> None:
    client = CosmoRealtime(api_key=os.environ["COSMO_API_KEY"])
    agent = client.agent(
        instructions="You are Alex from Acme, calling to confirm tomorrow's delivery window.",
        greeting="Hi, this is Alex calling from Acme about your delivery.",
        interruption_sensitivity="low",
        audio=AudioConfig(noise_cancellation=True),
    )

    async with agent.start() as session:
        result = await session.dial("+14155550199")
        print(f"dialing… dial_id={result.dial_id}")
        async for event in session:
            match event:
                case RealtimeTranscriptDelta() if event.is_final:
                    print(f"[{event.role.value}] {event.text}")

asyncio.run(main())

dial() returns as soon as the call is queued — the ring happens out of band, and the conversation shows up on the session's ordinary event stream (user-started-speaking, transcripts, tool calls). TypeScript is the same shape: await session.dial('+14155550199').

Numbers and caller ID

  • phone_number must be E.164 (+14155550199). The SDK validates the format locally before any network call; the server validates again.
  • caller_number (optional) sets the caller ID shown to the callee. It must be an active number in your workspace's phone pool — an unknown or inactive number is rejected with caller_number_not_available. Omit it to use the trunk default.
await session.dial("+14155550199", caller_number="+14155550100")

Prerequisites and limits

  • Phone calls must be enabled for your workspace (dashboard toggle); dialing without it fails with phone_calls_disabled.
  • Outbound minutes are subject to a weekly per-user limit — exceeding it fails the dial, not the session.
  • Dialing requires API-key auth (realtime:use); end-user token sessions cannot place calls.

Failure modes

dial() raises a typed error (DialError in Python, RealtimeDialError in TypeScript) with a code you can branch on:

CodeMeaning
invalid_phone_numbernot E.164 — caught locally before the request
phone_calls_disabledworkspace toggle is off
caller_number_not_availablecaller ID not in the workspace pool
minute_limit_exceededweekly outbound-minute cap reached
session_not_found / session_not_livethe session ended (or never existed) before the dial

A failed dial leaves the session itself alive — you can fix the number and dial again.

Designing the agent for the phone

Phone audio is narrowband, echoey, and full of half-listening humans. Configuration that works well:

  • interruption_sensitivity: "low" — don't yield the floor to "mm-hm" and line noise (turn-taking).
  • audio.noise_cancellation: true — suppress background voices.
  • greeting — the agent speaks first the moment the callee answers; a call that opens with silence gets hung up on.
  • Silence-timeout hooks — a Say nudge at ~30s and an EndCall at ~90s handle voicemail pickups and walked-away callers even if your process dies mid-call.
  • Call-control toolscosmo.end_call so the agent can hang up, and a transfer-call tool for human handoff: see In-call tools.

There is no phone "mode" to switch on — a phone agent is an ordinary agent whose configuration names each behavior it needs.

On this page