Outbound Phone Calls (VoIP)
Agents can place real outbound phone calls. The agent dials a number through Twilio and holds a live, interruptible spoken conversation powered by Gemini Live; after you hang up, the transcript flows back to the agent so it can act on what was discussed.
This is distinct from Voice Chat: voice chat is you talking to your agent in the browser; VoIP is the agent calling a phone number over the public telephone network.
This release is outbound only— agents place calls; they do not answer incoming ones.
Concepts
VOIP_ENABLED=true and a GEMINI_API_KEY on the platform. When off, every VoIP endpoint returns 404.voice) and, by default, dispatched to the agent as a task so it can follow up — update memory, create tasks, send messages.Requirements
Platform flags — In .env: VOIP_ENABLED=true and a valid GEMINI_API_KEY. Restart the backend after changing them.
Public URL — Settings → Public Chat URL must be set to your instance's public domain (e.g. https://your-domain.com). Trinity builds the audio WebSocket URL from it.
Public reachability — Twilio must be able to open a WebSocket to wss://your-domain.com/api/voip/voice/.... This works on publicly reachable deployments, e.g. with a Cloudflare Tunnel set up per Public Access. A catch-all tunnel route to the frontend needs nothing extra; a tunnel that routes specific paths directly to the backend must also route api/voip/*.
Per-agent Twilio binding — A Twilio account with a voice-capable phone number, configured on the agent (below). Without a binding, calls fail with 400 even when the flag is on.
How It Works
There is no UI for VoIP yet — binding configuration and call triggering are API/MCP-only.
1. Configure the Agent's Twilio Binding
The agent owner configures the binding:
curl -X PUT http://localhost:8000/api/agents/my-agent/voip \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"auth_token": "your-twilio-auth-token",
"from_number": "+15551234567",
"daily_call_cap": 20
}'AC, 34 characters. Trinity validates the credentials against Twilio before saving.GET the same endpoint to check binding status (returns the Twilio account's display name when configured); DELETE removes the binding.
2. Place a Call
Anyone with access to the agent (owner, admin, or shared) can trigger a call — most commonly the agent itself, via the call_user MCP tool:
mcp__trinity__call_user({
to_number: "+15557654321",
context: "Brief the user on today's pipeline status and ask about the Q3 budget decision."
})
// → { success: true, call_id: "voip_...", status: "ringing", twilio_call_sid: "CA..." }The optional context(up to 2,000 characters) becomes the call's purpose in the agent's voice prompt — the agent greets the person, says who it is and why it's calling.
3. During the Call
run_task tool mid-call — and announces it first (“let me check that”) instead of going silent during slow lookups.VOIP_MAX_CALL_DURATION).4. After the Call
voice.process_transcript: false to skip this.For Agents
MCP Tool
| Tool | Description |
|---|---|
| call_user | Place an outbound call. Params: to_number (E.164, required), context (≤2000 chars, optional), process_transcript (default true), agent_name (required for user-scoped keys; agent-scoped keys default to the bound agent) |
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
| /api/agents/{name}/voip | GET | Binding status (owner-only) |
| /api/agents/{name}/voip | PUT | Configure Twilio voice binding — validates credentials with Twilio, encrypts the Auth Token (owner-only) |
| /api/agents/{name}/voip | DELETE | Remove the binding (owner-only) |
| /api/agents/{name}/voip/call | POST | Place an outbound call; returns {call_id, status: "ringing", to_number, twilio_call_sid, chat_session_id} |
| /api/voip/voice/{call_id} | WebSocket | Twilio Media Streams audio bridge (ticket-authed, used by Twilio — not called directly) |
The call endpoint accepts an optional Idempotency-Key header, so a retried trigger never dials the same number twice.
curl -X POST http://localhost:8000/api/agents/my-agent/voip/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: briefing-2026-06-11" \
-d '{"to_number": "+15557654321", "context": "Daily status briefing"}'Common Errors
| Status | Meaning |
|---|---|
| 404 | VoIP is not enabled on the platform |
| 400 | No active binding, invalid phone number (must be E.164, e.g. +15551234567), or Public Chat URL not configured |
| 429 | Rate limit (default 5 calls per owner+destination per 60s) or daily call cap reached |
| 502 | Twilio rejected the call (e.g. unverified destination on a trial account) |
Configuration
| Variable | Description | Default |
|---|---|---|
| VOIP_ENABLED | Master switch | false |
| VOIP_MAX_CALL_DURATION | Hard per-call cap in seconds | 600 (10 min) |
| VOIP_DEFAULT_DAILY_CALL_CAP | Per-agent calls/day (overridable per binding) | 50 |
| VOIP_CALL_RATE_LIMIT / VOIP_CALL_RATE_WINDOW | Calls per owner+destination per window (seconds) | 5 / 60 |
GEMINI_API_KEY (shared with voice chat) must also be set.
Limitations
VOIP_MAX_CALL_DURATION.