Skip to content
Guide

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 · 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?

  1. 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 the host of 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.

  2. 02Install the SDK and connect the trunk

    Install the SDK and put your API key in the environment:

    shell
    npm install wixzel-voice
    export WIXZEL_API_KEY="wv_live_..."
    trunk.mjs
    import { 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 username and password. The password is stored encrypted and never returned by the API again.

  3. 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_ip printed 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_uri printed 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.

  4. 04Create the receptionist

    An agent is a prompt, an opening line and a voice engine. appointment_booking_enabled lets it list, book and cancel appointments during the call; the prompt tells it which days, hours and slot lengths it may offer.

    agent.mjs
    import { 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 voice to the tts block 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.

  5. 05Route your number to the receptionist

    Register the number on the trunk, with the agent as the one that answers it:

    number.mjs
    import { 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 with client.agents.update(agentId, { outbound_phone_number_id: number.id }).

  6. 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.mjs
    import { 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_time is always UTC. Each booking also fires the appointmentBooked webhook, 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.

Monthly engine cost for this guide’s build
Calls per monthConnected minutesEngine cost per month
200600$52.02
1,0003,000$260
3,0009,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_time of 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-Version header.

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 an inbound_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-voice on npm for TypeScript and JavaScript, and wixzel_voice on 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.

APIs for agentic telephony

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.