Build a Twilio AI receptionist in Node.js with Wixzel Voice
By the end, calls to your existing Twilio number are answered by an AI receptionist you built: it greets the caller, answers from your prompt and books appointments you can read back through the API. It takes about 30 minutes in Node.js, with the wixzel-voice SDK for TypeScript and JavaScript and a Twilio Elastic SIP trunk. Engine time on the classic engine is $0.0867 per connected minute, about $0.26 for a three-minute call, billed per second from a prepaid balance. Twilio bills the trunk minutes to you directly, with no markup.
By Aqeel Shamsudheen · Published · Updated
- Takes
- About 30 minutes
- Engine
- classic
- Engine time
- $0.0867 per connected minute
What do you need before you start?
- A Twilio account with Elastic SIP Trunking, and a Twilio phone number.
- A Wixzel Voice API key (
wv_live_…) and credit on the account. The minimum top-up is $5. - Node.js 20 or later.
How do you build this on Wixzel Voice, step by step?
01Create the Elastic SIP trunk at Twilio
In the Twilio console, open Elastic SIP Trunking and create a trunk. Under Termination, choose a Termination SIP URI, such as
your-clinic.pstn.twilio.com. That hostname is thehostof the trunk you connect in the next step.Keep the Twilio console open. You come back to it once the API has given you the two addresses to enter.
02Install the SDK and connect the trunk
Install the SDK and put your API key in the environment:
shellnpm install wixzel-voice export WIXZEL_API_KEY="wv_live_..."trunk.mjsimport { WixzelVoice } from 'wixzel-voice'; const client = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); const trunk = await client.sipTrunks.create({ name: 'Twilio', host: 'your-clinic.pstn.twilio.com', // your Termination SIP URI port: 5060, transport: 'udp', }); console.log('TRUNK_ID', trunk.id); console.log('Allowlist at Twilio:', trunk.platform_ip); console.log('Origination URI:', trunk.origination_uri);There is no username or password here because Twilio authorises by source IP, which is the next step. To use a Twilio Credential List instead, pass its
usernameandpassword. The password is stored encrypted and never returned by the API again.03Point Twilio at Wixzel Voice, in both directions
Twilio configures outbound and inbound separately, and each fails quietly when it is missing.
Under Termination, then Authentication, add the
platform_ipprinted above to an IP Access Control List. Without it Twilio rejects calls in about a second with hangup cause 21.Under Origination, add an Origination SIP URI set to the
origination_uriprinted above. Without it your number never reaches the agent and nothing appears in either log, because the call never leaves Twilio.Finally, attach your Twilio number to this trunk under Numbers.
04Create the receptionist
An agent is a prompt, an opening line and a voice engine.
appointment_booking_enabledlets it list, book and cancel appointments during the call; the prompt tells it which days, hours and slot lengths it may offer.agent.mjsimport { WixzelVoice } from 'wixzel-voice'; const client = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); const agent = await client.agents.create({ name: 'Reception', system_prompt: [ 'You are the receptionist for Riverside Dental, answering the phone.', 'Keep every reply to one or two short sentences, and never read out lists.', 'You can book, move and cancel check-ups and cleanings.', 'Offer appointments Monday to Friday, 09:00 to 17:00, 30 minutes each.', 'Before booking, ask for the full name of the caller and repeat the day and time back.', 'For a dental emergency, tell the caller to hang up and ring the emergency number.', 'If you cannot help, take a message and promise a callback.', ].join('\n'), opening_message: 'Riverside Dental, this is the automated assistant. How can I help?', appointment_booking_enabled: true, voice: { stt: { model: 'deepgram/nova-3', language: 'en-US' }, llm: { model: 'openrouter/gpt-4o-mini' }, tts: { model: 'elevenlabs/eleven_turbo_v2_5' }, }, }); console.log('AGENT_ID', agent.id);This is the classic engine: Deepgram Nova-3, GPT-4o-mini and ElevenLabs Turbo v2.5, the cheapest reliable pairing for English at $0.0867 per minute. Add a
voiceto thettsblock to choose the speaker;client.engines.voices('classic')lists the ones it can use.The opening line says the caller is talking to an automated assistant. Where the law requires that, or requires notice that a call is recorded, the opening line is where it belongs.
05Route your number to the receptionist
Register the number on the trunk, with the agent as the one that answers it:
number.mjsimport { WixzelVoice } from 'wixzel-voice'; const client = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); const number = await client.phoneNumbers.create({ phone_number: '+14155550100', // your Twilio number, in E.164 sip_trunk_id: process.env.TRUNK_ID, inbound_agent_id: process.env.AGENT_ID, }); console.log('NUMBER_ID', number.id);Without an
inbound_agent_id, inbound calls to a number are rejected. To let the same agent place calls as well, for reminders or a test call, make the number its caller ID withclient.agents.update(agentId, { outbound_phone_number_id: number.id }).06Call it, then read the call and the booking
Ring your Twilio number from a mobile and book a slot. When you hang up, the call and the appointment are both in the API:
results.mjsimport { WixzelVoice } from 'wixzel-voice'; const client = new WixzelVoice({ apiKey: process.env.WIXZEL_API_KEY }); // Lists are newest first. const calls = await client.calls.list({ direction: 'inbound', limit: 1 }); const latest = calls.data[0]; if (latest) { const call = await client.calls.retrieve(latest.id); console.log(call.status, call.duration_seconds + 's', (call.cost_micros ?? 0) / 1e6, 'USD'); for (const line of call.transcript) console.log(line.role + ': ' + line.content); } const appointments = await client.appointments.list({ limit: 5 }); for (const a of appointments.data) { console.log(a.date_time, a.client_name, a.phone_number, a.status); }date_timeis always UTC. Each booking also fires theappointmentBookedwebhook, which is how you would copy it into your own calendar as it happens.
What does this build cost on Wixzel Voice?
Engine time for 3-minute calls on the classic engine at $0.0867 per connected minute: about $0.26 per call. Telephony is your own trunk at your carrier’s rate and is not included.
| Calls per month | Connected minutes | Engine cost per month |
|---|---|---|
| 200 | 600 | $52.02 |
| 1,000 | 3,000 | $260 |
| 3,000 | 9,000 | $780 |
Wixzel Voice bills engine time only, per second of the call, from a prepaid balance. Twilio bills the Elastic SIP Trunking minutes to your Twilio account at your Twilio rates; Wixzel Voice never sees or marks up that charge.
Prepaid, with no subscription and no free tier: add credit from $5 and usage spends it. Estimate your own mix.
Which Wixzel Voice limits apply to this build?
- The receptionist learns your hours from its prompt. The v1 API has no availability endpoint, so it cannot see your real calendar: a time already booked through the API is refused, but appointments you keep elsewhere are invisible to it.
- Bookings are read in the account's time zone, which is UTC unless it has been changed, and the v1 API cannot change it yet. Check the
date_timeof your first bookings before relying on them, and write to aqeel@wixzel.com if your business is not on UTC. - Five calls can be live at once per account by default. Further calls are refused until one ends, and the limit is raised on request.
- Your number stays at Twilio. Wixzel Voice does not sell, port or hold numbers.
- Wixzel Voice is in alpha. Behavioural changes ship behind a dated
Wixzel-Versionheader.
Frequently asked questions
- Can I build an AI receptionist on Twilio?
- Yes. Connect your Twilio Elastic SIP trunk to Wixzel Voice, allowlist the platform IP under Termination, set the Origination SIP URI, and register your Twilio number with an
inbound_agent_id. Calls to the number are then answered by your agent, and Twilio keeps billing the trunk to you directly. - Do I have to move my number away from Twilio?
- No. The number stays in your Twilio account, attached to your Elastic SIP trunk. Wixzel Voice does not sell, port or hold numbers.
- Why does my Twilio number not reach the agent?
- Usually the Origination SIP URI is missing, or the number is not attached to the trunk. Inbound delivery is configured separately from outbound, and without it Twilio has nowhere to send the call, so nothing appears in either log. The URI must match the trunk's
origination_uri, and the number needs aninbound_agent_id. - What does an AI receptionist call cost?
- On the classic engine, $0.0867 per connected minute, about $0.26 for a three-minute call, billed per second from prepaid credit. Twilio's trunk minutes are billed by Twilio at your rates.
- Is there an SDK for languages other than JavaScript?
- There are two official SDKs:
wixzel-voiceon npm for TypeScript and JavaScript, andwixzel_voiceon pub.dev for Dart and Flutter. Other languages call the same HTTP API directly, described by the OpenAPI document at https://docs.voice.wixzel.com/api-reference.
Where to read more
- SIP trunksdocs
The Twilio section: IP ACL, Termination URI, caller ID and the Origination URI.
- Agentsdocs
Prompts, opening lines, where each field value comes from, and test calls.
- Webhooksdocs
appointmentBooked, callCompleted, signatures and retries.
- SDKsdocs
The TypeScript and Dart SDKs.
About Wixzel Voice
Wixzel Voice is a voice AI API for building AI agents that place and answer real phone calls over your own SIP trunk, or talk to people in your web and mobile apps. One API key and one prepaid balance cover every voice engine, billed per second of actual usage.
More guides
Claude, in Claude Code or claude.ai, placing a real call over your SIP trunk and reading back the transcript, with a confirmation before anything dials.
Two phone agents, one speaking Hindi and one Malayalam, on Sarvam's Indian-language models, answering or placing calls over your SIP trunk.
A Flutter app where users talk to your voice agent over a WebSocket, with the API key kept on your server and no phone number involved.
A Gemini Live agent ringing a real phone over your SIP trunk from four cURL requests, and the call record with its transcript and cost.
A list of leads called by your agent over your SIP trunk, in batches sized to your concurrency limit, with every call's status and transcript read back.
