Build a voice agent
Answer phone calls with an AI agent that says it is AI, runs your actions as tools, and never records without consent.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will route a phone number to a voice session for one of your agents. It takes about 30 minutes and needs a telephony account and a speech-to-speech provider key for a live call. Tests run with no account, against local room servers and a real WebSocket on the stream route.
You need
- An agent declared under
agentsinmain.yaml, with a budget. - A Twilio, Telnyx or SIP trunk account, and an OpenAI or Google key for the realtime model.
- The provider hosts in
network.allow.mainsdk checkreports any that are missing asMAIN-VOICE-0011.
1. Configure
agents:
support: { budget: { spend: $5/day, calls: 500/day } }
ai:
driver: openai
models: [openai/*]
voice:
driver: openai-realtime
disclosure: You are speaking with an AI assistant for Pinecart.
max-duration: 15m
telephony:
driver: twilio
account-sid: secret:TWILIO_SID
auth-token: secret:TWILIO_TOKEN
webhook-url: https://app.example.com/voice
stream-url: wss://app.example.com/voice/stream
stream-secret: secret:VOICE_STREAM
numbers: { "+15550100": support }
sessions:
support: { voice: alloy, instructions: "Help callers with their orders.", feature: voice-support, agent: support }A number routed to a session with an empty disclosure fails mainsdk check with MAIN-VOICE-0010.
2. Answer calls
import { ai } from "mainsdk";
export async function POST(req: Request) {
return ai.voice.calls.handle(req); // verifies the signature, picks the session by the number called
}An unsigned or forged webhook fails with MAIN-VOICE-0005 and nothing runs. The call state is read back from the provider, never taken from the webhook body.
3. Serve the media stream
The webhook answer tells the carrier to stream the call's audio to stream-url, with a token for that one call in the path. Your HTTP server hands the upgrade to the session:
import { createServer } from "node:http";
import { ai } from "mainsdk";
const server = createServer(/* your request handler */);
server.on("upgrade", (req, socket, head) => {
if (req.url?.startsWith("/voice/stream/")) {
void ai.voice.calls.upgrade({ req, socket, head }); // runs until hangup or a limit
}
});A stream without a valid token for a call that is waiting gets a 401 with MAIN-VOICE-0005 before the socket opens. MainSDK runs the WebSocket itself: no SIP stack and no SFU. On a runtime with its own upgrade API, accept the socket there and pass it to ai.voice.calls.attach(callId, socket).
4. Put the agent in a room (optional)
For a LiveKit or Daily room, add ai.voice.rooms with the provider keys and a bridge: the wss:// URL of the media bridge you run next to the room, which carries 16 kHz PCM audio (the providers use WebRTC, which MainSDK does not run).
import { ai, type AiUser } from "mainsdk";
declare const options: { voice: string; instructions: string; feature: string; agent: string };
declare const user: AiUser;
const session = await ai.voice.rooms.join({ room: "r1", session: { ...options, user } });
const token = await ai.voice.rooms.token({ room: "r1", user }); // for the client SDK, 10 minutesA join with no bridge fails with MAIN-VOICE-0002, and a bridge that never confirms fails with MAIN-VOICE-0006.
Check it worked
The first event of every session is a disclosure that the caller is talking to AI. In a test, ai.driver: local and a fake voice driver give the whole conversation offline. A session that reaches max-duration or its cost limit says a closing line and ends with MAIN-VOICE-0004. Recording is off unless you set record: consent and the caller's call-recording consent is recorded; otherwise it fails with MAIN-VOICE-0003 and the call goes on unrecorded.
Next
Was this page helpful?