--- title: Replace Your Phone Menu With a Voice Agent description: Press 1 for sales is a routing table people have to read out loud. Build a voice agent that answers, understands, does the work, and hands off to a human when it should. date: 2026-07-26 author: Victor Villalobos locale: en source: https://www.zavu.dev/en/blog/replace-your-ivr-with-a-voice-agent tags: Voice Agents, IVR, AI Agents, Tutorial --- # Replace Your Phone Menu With a Voice Agent Every phone menu is the same apology. Press 1 for sales. Press 2 for support. Press 3 to hear these options again. It is a routing table, read out loud, at the speed of speech. The caller already knows what they want. They just have to wait while your org chart is recited at them, then guess which branch a person like them belongs in. The alternative is not a better menu. It is no menu: the phone is answered, the caller says what they need, and something on the other end understands, does the work, and puts a human on when a human is actually required. That is a voice agent, and it is roughly an afternoon of work. ## What the old phone system actually costs Not the line rental. Three other things. **Abandoned calls.** Every layer of menu loses people. They hang up and try a competitor, or they pick the wrong branch and land on someone who cannot help them, which is worse because now two people wasted the time. **No record of what anyone wanted.** An IVR knows the caller pressed 2. It does not know they were asking about a refund on an order from March. That intent is the most valuable thing in the call and your phone system throws it away every time. **It cannot do anything.** A menu routes. It cannot check a booking, quote a price, or update an address. Everything is a handoff to a person, including the half of calls that a person will resolve by reading one field off a screen. A voice agent fixes all three for the same reason: it is a conversation, so it hears intent, and it has tools, so it can act on it. ## What you are building | The old menu | A Zavu voice agent | |--------------|--------------------| | Press 1 for sales | The caller says what they want | | A tree you maintain in a portal | A prompt in your repo, in version control | | Routes, then hangs up on the branch | Looks things up and answers mid-call | | Transfers blind | Transfers when your prompt says to, to the number you set | | Knows a digit was pressed | Full transcript, duration, and cost per call | The agent is not a separate product from the one answering WhatsApp. It is the same agent with a `voice` block: same prompt, same model, same tools, same knowledge base. ## Before you start Three prerequisites, and one of them takes a day. - **A sender with a phone number.** That number is what people dial. - **Voice Agents enabled for your team.** It is in beta and turned on per team, so the voice endpoints return `403` until someone flips it. Email [hi@zavu.dev](mailto:hi@zavu.dev) and ask. - **A positive balance.** Calls are billed per connected minute. Inbound calls are only answered while your balance is above zero, which is a failure mode worth knowing before it happens on a Monday. ## Who does what You bring the account, the number, and the judgment about when a human should pick up. Your coding agent writes and ships the rest. | You | Your coding agent | |-----|-------------------| | Create the Zavu account | Finds or creates the sender | | Run `zavu login` and authorize | Writes `defineAgent` with the voice block | | Ask for Voice Agents to be enabled | Writes the tools it calls mid-call | | Approve buying a phone number | Sets the secrets and deploys | | Take the first test call | Reads the transcript and fixes the prompt | ## The only part you type ```bash npx skills add zavudev/zavu-skills npx zavudev login ``` The first command installs Zavu's skills into your coding agent. Claude Code, Cursor, Copilot, Codex and 40 or so others are supported; the installer asks which. One of those skills is `voice-agent`, which is what stops your agent from inventing config fields that do not exist. `login` opens a browser, you pick the project, and the key lands in `~/.zavu/credentials.json`. From here you describe what you want. ## Then you describe the agent ``` Build a Zavu voice agent for our clinic on my existing sender. It should answer in Spanish, tell people our hours, look up an appointment by phone number, reschedule it, and transfer to +14155551234 when someone asks for a person or sounds upset. Twelve minute cap. Deploy it. ``` You do not need to know the commands below. They are here so you recognise what scrolls past, and so you notice when a step got skipped. ```bash npx zavudev whoami # which project am I about to change npx zavudev senders list # the number people will dial npx zavudev fn secrets set SENDER_ID "jn76vnxet8g5nq661by3v06y1581bmmn" npx zavudev deploy ``` If you would rather start from something proven, there is a catalog of ready-made voice agents: ```bash npx zavudev agents catalog ``` ``` id name voice tools category fermi Fermi, Lead Qualification yes 5 sales curie Curie, Customer Support yes 5 support kepler Kepler, Video Call Booking yes 2 frontDesk hopper Hopper, Lead Capture and Booking yes 5 sales aria Aria, Multi-channel concierge yes 3 frontDesk atlas Atlas, Product Expert yes 0 support ``` `npx zavudev agents pull curie --sender "$SENDER_ID"` scaffolds one into your repo as editable TypeScript. It is a starting point you own, not a black box you rent. ## Read what it wrote ```ts import { defineAgent, defineTool } from "@zavudev/functions" defineAgent({ senderId: process.env.SENDER_ID!, name: "Clínica Norte", provider: "zavu", model: "openai/gpt-4o-mini", channels: ["voice", "whatsapp"], voice: { enabled: true, model: "openai/gpt-4o", greeting: "Clínica Norte, buenos días. ¿En qué le puedo ayudar?", language: "es", interruptible: true, maxCallDurationMinutes: 12, maxIdleSeconds: 30, transferPhoneNumber: "+14155551234", voicemailAction: "hangup", }, prompt: "You are the front desk of Clínica Norte. Answer in Spanish, one or two sentences. Transfer to a human when the caller asks for one, or when they are upset.", }) ``` Five fields decide whether the call feels human. `interruptible` is barge-in. The caller can cut the agent off mid-sentence and it stops to listen. Leave it on. An agent that talks over people is the thing everyone already hates about robocalls. `maxIdleSeconds` ends the call after silence, so a phone left face-down in a bag does not bill you for twelve minutes. `maxCallDurationMinutes` is the hard stop. It exists because a conversation that has gone wrong tends to go wrong for a long time. `voicemailAction` decides what happens when an answering machine picks up on an outbound call: `hangup` silently, or `leave_message` and speak. Leaving a message when you meant to reach a person is how a helpful agent becomes a nuisance. `transferPhoneNumber` is the escape hatch, and it deserves the next section to itself. ## The transfer is the part people judge you on Set `transferPhoneNumber` and the agent gets a transfer tool. It also always gets a hangup tool, so it can end a finished call on its own. Two things to know, because the second one surprises people. The transfer only exists if the sender also has a phone number to dial out from. Set the target, forget the sender's number, and the agent will have nothing to escalate with and no way to say so. **A transfer forwards the call. It does not brief the human.** Your colleague picks up a ringing phone with no idea the caller already spent ninety seconds explaining themselves. If you want them to know, that is yours to build: the full transcript is on the call record and in the `call.completed` webhook, so you can push a summary to Slack, WhatsApp, or your helpdesk as the transfer happens. Say it in your prompt, too, so the agent tells the caller what is about to happen instead of going quiet. ## Give it something to do A menu routes. This is the part that makes an agent worth more than a menu: ```ts defineTool({ name: "find_appointment", description: "Look up a patient's next appointment by phone number.", parameters: { type: "object", properties: { phone: { type: "string", description: "E.164 phone number" } }, required: ["phone"], }, handler: async ({ phone }) => { const res = await fetch("https://api.clinic.example/appointments?phone=" + phone) if (!res.ok) return { found: false, reason: "lookup_failed" } return { found: true, ...(await res.json()) } }, }) ``` Worth stating plainly: **tools do run on voice.** On the plain text path they do not, which trips people up in the other direction. A voice agent can genuinely look things up mid-sentence, and that is the whole reason to point one at your phone number. Test the handler on its own before any call: ```bash npx zavudev fn invoke --tool find_appointment --args '{"phone":"+14155551234"}' ``` That runs on your machine, against uncommitted code, in under a second. ## Hear it before a customer does Two different checks, and they catch different failures. ```bash npx zavudev agents test --sender "$SENDER_ID" --message "quiero cambiar mi cita del martes" ``` This exercises the brain: prompt, model, knowledge base. Nothing is delivered, nothing is charged, nothing is recorded. It also prints warnings about what a dry run cannot prove, and on a voice agent the most useful one is that this path does not call tools even though the call will. Then call yourself: ```bash npx zavudev calls create --to "+1YOUROWNNUMBER" ``` Your phone rings and the agent talks to you. Nothing else tells you the greeting is three seconds too long, or that the voice reads a phone number as one enormous integer. Listen for interruption handling: talk over it on purpose and see whether it stops. Then read the call back: ```bash npx zavudev calls list --limit 5 npx zavudev calls get ``` `list` shows direction, status, duration, and why each call ended. `get` returns the turn-by-turn transcript. That transcript is the thing your old phone system never gave you, and it is where the next version of the prompt comes from. Then hand the loop over: ``` Read the transcript of my last three calls. Every caller asked about parking and the agent did not know. Add it to the knowledge base, fix the greeting to be shorter, and redeploy. ``` ## It calls out, too The same agent places calls: ```bash npx zavudev calls create --to "+56912345678" --language es-ES --max-minutes 5 ``` Appointment reminders, delivery windows, a payment that failed. `--language` accepts `auto` to follow the caller. `voicemailAction` decides what happens when a machine answers, which on outbound campaigns is most of the time. Outbound is also where an agent stops being a convenience and starts being a thing that can annoy thousands of people quickly. Reminders people asked for, yes. Cold calls, no. Consent and calling-hours rules are yours to respect, and neither the model nor the API will do it for you. ## What happens on every call Four webhook events fire: `call.initiated`, `call.answered`, `call.completed`, `call.failed`. Every one carries the call id, direction, from, to, status, duration, and why it ended. The terminal two also carry `cost`, in USD, after the call has been charged. That is not the raw carrier number: it is what you were actually billed, telephony and the voice pipeline together, so you can put a real price next to a real conversation. ## How Zavu bills this **Voice is per connected minute.** The managed pipeline is $0.0625 a minute plus telephony, counted from answer to hangup. Standard models are covered by that rate; a premium model adds its own token cost. A short estimate is reserved when the call is placed and settled against the real duration when it ends. **The plan covers everything around it.** Free is $0, Hobby is $25 a month, and the rest scale from there. Each plan includes a number allowance, three on Hobby, and the first US number is free for every team. Extra numbers are about a dollar a month each. **Building is free.** Skills, CLI, SDKs, `deploy`, and every redeploy. `agents test` charges nothing. `fn invoke` runs locally. What you pay for is a connected call. The one number to hold on to: a five minute call on the managed pipeline is about thirty cents plus telephony. Compare that against what a person costs for the same five minutes, and then against what an abandoned call costs, which is the whole order. Rates move. [The pricing page](/pricing) is the one that is current. ## Three rules that keep this honest **Never fake a success.** A tool that reports a rescheduled appointment it never rescheduled makes the agent tell a real person, out loud, in a voice that sounds confident, that their Tuesday is handled. Return the failure. The agent will say so, and the caller can do something about it. This is worse on voice than anywhere else, because nobody re-reads a phone call. **Say when you are transferring.** Silence followed by ringing feels like being hung up on. One sentence, then the transfer. **A dry run is not a call.** `agents test` proves the prompt is sane. It does not prove the greeting sounds right, that the voice pronounces your company name correctly, or that the agent stops when interrupted. Only a real call proves that, so make the first one to your own phone. ## Where this leaves you Your phone number now answers, understands, checks something real, and gets a person on the line when it should. The behaviour lives in a file you can diff, with a transcript for every call and a cost next to it. The menu was never the product. It was the compromise you made because software could not hold a conversation. It can now. - [Voice Agents overview](https://docs.zavu.dev/guides/voice-agents/overview) - [Voice quickstart](https://docs.zavu.dev/guides/voice-agents/quickstart) - [Voice configuration](https://docs.zavu.dev/guides/voice-agents/configuration) - [Voice webhooks](https://docs.zavu.dev/guides/voice-agents/webhooks) - [Factory agents](https://docs.zavu.dev/guides/functions/factory-agents) - [Coding Agent Skills](https://docs.zavu.dev/tools/coding-agent-skills)