Bland becomes FedRAMP certified, clearing the highest security standards.

Back to blog

How to Make a Text to Speech Phone Call With Node.js

Learn how to make a text to speech phone call with Node, and avoid the infrastructure gaps that stall production voice builds for ops teams.

Ethan ClouserUpdated September 5, 202618 min read

Node.js makes your first TTS phone call easy. This guide shows what breaks when you move past localhost, and how to close the gap before production does it for you.

Building a text-to-speech phone call with Node.js looks deceptively simple on paper. Most operations and revops leaders think that a working Twilio + Node.js TTS call means the hard part is done, that everything else is just polish. You find a tutorial, wire up an SDK, pass a string to a voice API, and within twenty minutes you're hearing synthesized audio on the other end of the line. What the tutorial doesn't show you is the infrastructure gap waiting on the other side of localhost, and that gap is where real projects stall.

Our own research found that most TTS models are trained on professional recordings such as audiobooks, podcasts, and voiceovers, which teach polished cadence but not the fragmented, self-correcting nature of real conversation.

Pipeline diagram showing where TTS phone call stacks break between synthesis and telephony

A text-to-speech phone call starts as a string of characters. A synthesis engine converts that string into a waveform, typically an MP3 or WAV payload, and a telephony layer transmits that audio across a carrier network to a physical phone. Three distinct systems have to coordinate in sequence: your application, the voice synthesis provider, and the public switched telephone network. Each handoff introduces latency, and each system has its own failure modes. The carrier network operates under conditions your development environment never simulates, including SIP negotiation delays, codec mismatches, and regional routing variability that can add hundreds of milliseconds between synthesis and delivery.

JavaScript has been the most commonly used programming language for twelve consecutive years, according to the Stack Overflow 2024 Developer Survey. Node.js handles concurrent I/O without blocking the event loop, which matters enormously when you're managing outbound call state across dozens of simultaneous webhook callbacks. A telephony integration lives and dies on webhook handling: Twilio calls your endpoint to fetch TwiML, your server responds, and the call proceeds. Node's non-blocking architecture means one slow carrier response doesn't freeze every other in-flight call.

Key takeaways#

  • A working Twilio + Node.js TTS call in twenty minutes is real, the production wall hits later, when concurrency, compliance logging, and call-state persistence land entirely on your team to build.
  • Hardcoding credentials is the first architectural mistake most developers make, and automated scanners find exposed Auth Tokens in public repos faster than most teams rotate them.
  • The TwiML Bin vs. Express Server choice isn't a convenience preference, it's a commitment that determines how much call-control logic you can ever ship.
  • ngrok solves the localhost problem for demos; it introduces a new class of reliability and latency issues the moment real call volume shows up.
  • Scaling a DIY TTS stack means owning retry logic, provider credential rotation, concurrent call queuing, and compliance recording, none of which ship with the SDK.
  • bland.ai's Programmable Voice Agents API closes that gap by treating latency, compliance, and call control as first-class concerns from line one, not features you bolt on after the demo works.

Configuring API Credentials - Your Twilio Account SID, Auth Token, and .env Setup#

Paste your credentials into the code, ship the demo, clean it up later. That plan sounds reasonable until 2 a.m. when you're rotating a compromised Auth Token because an automated scanner found it in your public repo before you did. Credential setup is the first architectural decision that determines whether your voice app can survive production scrutiny. A developer who treats credential hygiene as an afterthought has already failed the operational readiness audit before a single call is placed.

"API credentials like Twilio Auth Token and Account SID are stored in .env files with no scoping, the agent has access to all keys all the time, creating a broad attack surface."

Old way of hardcoding credentials versus new secure .env setup for voice AI apps

That distinction matters most in compliance-heavy industries. The same automated scanners that exploit a leaked key within minutes are precisely the class of threat that healthcare, finance, and legal vendors are evaluated against during procurement vetting. Skipping this step is how voice projects turn into incident reports.

The risk is concrete. A leaked Twilio Account SID and Auth Token, exposed through a third-party integration or an accidentally committed `.env` file, can lead directly to account compromise, unauthorized API access, and toll fraud charges. Real incidents have resulted in fraudulent charges reaching as high as $4,700 from a single breach. Rafter documents how quickly this damage accumulates once a secret is in the open: automated bots continuously scan public repositories and live endpoints, and a credential that goes public, even briefly, is treated as permanently compromised.

There is a second, subtler risk that developers building voice agents routinely underestimate. When API credentials like a Twilio Auth Token and Account SID are stored in a flat `.env` file, the agent or any process it spawns has access to every key all the time, with no scoping, no expiry, and no least-privilege enforcement. That broad attack surface means a single vulnerability anywhere in the call stack can expose credentials for your entire account.

Where to Find Your Twilio Account SID and Auth Token#

Your Twilio Account SID and Auth Token live on the main dashboard at console.twilio.com. Log in, and both values appear in the "Account Info" panel on the project homepage. The Account SID starts with `AC`; the Auth Token is masked by default and revealed with a single click. Copy both immediately into your `.env` file, nowhere else.

The Complete .env File Structure for a TTS Phone Call Project#

A minimal `.env` file for this project needs four variables:

``` TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_here TWILIO_PHONE_NUMBER=+15550001234 TO_PHONE_NUMBER=+15559876543 ```

As documented in a GitHub Gist (win3zz), these four variables cover every credential and number reference your call logic will need. Keep the file flat, keep it in the project root, and never commit it. For teams scaling toward production, particularly those handling after-hours or overflow call volume without adding permanent headcount, this flat structure eventually needs to give way to scoped secrets management, because an agent that runs continuously across outbound campaigns and 24/7 inbound handling should never hold broader permissions than the single task it is executing at that moment.

Why Credentials Belong in .env, Not in Your Source Code or Shell History#

Hardcoding API credentials in source code is among the most common causes of API key exposure. Automated scanners can identify and exploit a committed secret within minutes of a repository going public. Rafter confirms that the window between exposure and exploitation is measured in minutes, not days, which means the only safe assumption after a public commit is that the credential is already in adversarial hands.

For teams building on platforms like bland.ai, where integrations connect AI voice agents to live call flows across both inbound and outbound operations, this is not an abstract concern. A compromised credential does not just expose one demo; it exposes every call, every knowledge base, and every downstream integration the agent has been provisioned to reach. Enterprise deployments on bland.ai address this through dedicated infrastructure, compliance documentation available under NDA, and SSO, controls that reduce the blast radius of any single credential failure. But those protections only matter if the credential discipline that precedes them is already sound. The `.env` file is where that discipline either begins or breaks.

Setting Up Your Node.js Project and Making an Outbound TTS Phone Call#

The project structure you are about to scaffold sits on top of those loaded environment variables, and the outbound call logic will feel straightforward. Three commands separate "no project" from "first outbound call made." That speed is real, and it earns the momentum you feel when the phone rings. What it hides is that every parameter in those three commands encodes a decision you will eventually have to defend in production.

A JS TTS script that "works" is almost always written by someone who has made the call but has never operated it at scale. A JS TTS tutorial is one search away, but that very ubiquity masks the structural gap: the code below will run, and running it is not the same as understanding what you are committing to.

Three numbered steps from npm init through dotenv and Twilio SDK to first outbound call

Scaffold the Project in Three Commands: npm init, dotenv, and the Twilio SDK#

Run `npm init -y` to generate a default `package.json`, then `npm install dotenv` and `npm install twilio`. That is the complete dependency surface for a working outbound TTS call. dotenv is the right choice here for a concrete reason: hardcoding credentials into source files has caused real production incidents. Teams who committed an Auth Token to a public repo learned that lesson the hard way. Keep your `.env` file in `.gitignore` from the first commit, not as an afterthought.

What Each Line of make-call.js Actually Does (Annotated, Copy-Paste Ready)#

```js require('dotenv').config(); // loads TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, etc. from .env

const twilio = require('twilio');

const client = twilio( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN );

client.calls.create({ to: process.env.TO_PHONE_NUMBER, // destination in E.164 format, e.g. +12125551234 from: process.env.TWILIO_PHONE_NUMBER, // your provisioned Twilio number url: 'https://your-twiml-url', // Twilio fetches this when the call connects }).then(call => console.log('Call SID:', call.sid)).catch(err => console.error(err)); ```

Every line is load-bearing. The `url` parameter is not optional decoration; it is the address Twilio calls out to the moment the recipient picks up. That URL must return valid TwiML, the XML markup language Twilio uses to instruct a call what to do, such as reading text aloud via its `<Say>` verb.

How client.calls.create Hands Off to Twilio's Telephony Infrastructure#

`create` sends a POST request to `/2010-04-01/Accounts/{AccountSid}/Calls`. Twilio's infrastructure dials the `to` number and returns a Call resource with `status` set to `queued`.

That status confirms Twilio accepted the request and placed it in its dialing queue. Actual call delivery depends on carrier routing, the recipient's device state, and whether your TwiML URL responds correctly when Twilio fetches it.

TwiML Bin vs Express Server - Which Implementation Method Fits Your Use Case#

The choice between a TwiML Bin and an Express server looks like a developer-convenience question. It is not. It is an architectural commitment that determines how much call-control logic you will ever be able to ship.

Side-by-side comparison of static TwiML Bin limitations versus dynamic Express server capabilities for call control

TwiML Bin Is the Right Tool Until Your Message Needs to Change#

A static TwiML Bin earns its keep in exactly one scenario: your message never changes. Fixed appointment reminders with no personalization, internal test calls, or proof-of-concept demos where you just need a phone to ring and a voice to speak. Per industry documentation, TwiML Bins are hosted XML snippets that require zero server infrastructure. No server to provision, no webhook URL to manage, no uptime to worry about. For a static use case, that simplicity is the correct trade-off.

The problem surfaces the moment "static" stops being true.

The Moment a Bin Breaks Down#

A TwiML Bin cannot read a database. It cannot say "Hello Sarah, your order ships Thursday." It cannot handle DTMF input or conditional branching. Teams that start with a bin and later need personalization, DTMF handling, or conditional branching do not extend the bin. They rewrite everything. That rewrite is the cost nobody budgets for at the start.

Building a VoiceResponse That Returns Valid TwiML at Runtime#

A dynamic Express server returns TwiML XML at runtime using `twilio.twiml.VoiceResponse`. Here is a complete, working `server.js`:

```javascript require('dotenv').config(); const express = require('express'); const twilio = require('twilio');

const app = express(); app.use(express.urlencoded({ extended: false }));

app.post('/voice', (req, res) => { const twiml = new twilio.twiml.VoiceResponse(); twiml.say( { voice: 'alice', language: 'en-US' }, 'Hello. Your appointment is confirmed for tomorrow at 10 a.m.' ); res.type('text/xml'); res.send(twiml.toString()); }); app.listen(3000); ```

Running and Testing the Application - How to Set Up ngrok and Handle DTMF Responses

Getting audio to play is the easy win. The moment you add a keypress menu or branch on caller input, you discover a set of infrastructure problems that the "hello world" tutorial never mentions.

Twilio webhook branch showing outbound trigger succeeding and DTMF callback blocked without ngrok tunnel

Why Twilio Can't Call Your Laptop Back - The Public Webhook Requirement Explained#

Twilio's servers need a publicly reachable URL to deliver call events. Your laptop's `localhost:3000` address is invisible to the public internet, which means Twilio has nowhere to send the HTTP callback when a caller presses a key or the call connects. This is not a configuration quirk you can work around with a clever header. It is a hard architectural constraint: Twilio's infrastructure requires a public HTTPS endpoint to fire any webhook, including the initial TwiML fetch, DTMF callbacks, and call-status updates.

A pattern beginners run into here involves misreading which leg of the call is working. Builders who confirmed the call fires correctly from `make-call.js` assume the server is reachable. It is, for the outbound trigger. The return trip, when Twilio tries to call back with caller input, hits a wall.

Exposing Your Local Express Server in 60 Seconds with ngrok http 3000#

ngrok solves the reachability problem by creating a secure tunnel from a public HTTPS endpoint to your locally running server. Run `ngrok http 3000` in a separate terminal, and ngrok returns a URL like `https://abc123.ngrok.io` that proxies all traffic to your local Express process. Per the ngrok Documentation, Free Plan Limits, this tunnel makes your local server reachable by external services like Twilio that cannot access private addresses on a developer's machine.

Two constraints matter immediately. First, the free plan assigns a random, ephemeral URL every time ngrok restarts. Every restart means a new URL, which means every Twilio webhook reference in your code breaks, requiring you to manually update endpoint references before tests can resume, adding real friction to your development loop.

Second, and more critically: developers working with public tunnel endpoints routinely skip Twilio signature validation on those webhook routes. That is not a minor oversight. A publicly reachable endpoint that does not verify the `X-Twilio-Signature` header accepts any POST request, including crafted malicious payloads that can drain your Twilio balance or trigger unintended agent actions.

The ngrok Documentation, Free Plan Limits makes clear that free tunnels impose no authentication on inbound traffic by default, which means signature validation on your Express handler is the only layer standing between your endpoint and forged requests. These are solvable problems in a local prototype, but they illustrate precisely why production voice systems, particularly those handling complex, multi-step regulated calls end-to-end, require infrastructure built to enforce these controls by default rather than by developer memory.

DTMF Input Is Where Static TwiML Hits Its Ceiling - Using Gather to Capture Keypresses#

Static TwiML served from a Twilio Bin can read a message. It cannot react to what the caller does next. The `<Gather>` verb changes that: it tells Twilio to collect keypad input during a call and POST the result to a second endpoint you define, where your Express server reads the digit pressed and returns a new TwiML response branching accordingly. This is the point at which a static Bin becomes a hard blocker, and a dynamic server becomes a requirement.

It is also the point at which the gap between a local proof-of-concept and a production-grade voice system becomes concrete. Managing ephemeral tunnel URLs, enforcing webhook signature validation, and wiring multi-step call branches manually are all solvable, but they consume engineering cycles that compound quickly at scale. Teams handling high call volumes or 24/7 inbound coverage without scaling headcount find that this per-endpoint maintenance work grows linearly with every new call flow added. Platforms like Bland.ai address this by embedding conversational pathways, real-time transcription, and premium voice handling directly into a per-minute runtime, so the infrastructure concerns surfaced by `<Gather>` and ngrok tunnels are handled at the platform layer rather than in your own webhook handlers.

What the Tutorial Skips - The Hidden Costs of Scaling a DIY TTS Call Stack#

Getting a Node.js TTS call working in a tutorial environment and getting it to hold up under real call volume are two entirely different problems. The single-threaded event loop that makes Node.js feel fast and simple at low concurrency becomes a bottleneck the moment webhook round-trips, TwiML parsing, and audio synthesis requests start stacking on top of each other. What follows breaks down exactly where unmanaged DIY stacks break, and why those failure points matter before you commit to building on top of them.

DIY Node.js TTS pipeline breaking at audio synthesis stage under real call volume

Unmanaged Node.js TTS Stacks Queue and Drop Calls at Scale#

Our own research found that evals are positioned as a QA and compliance scoring tool for teams that need to audit failure modes across calls at scale without manual intervention (our data).

Getting a Node.js TTS call working means the hard part is done, everything else is just polish. It is not. Node.js runs on a single-threaded event loop.

That design handles async I/O elegantly at low volume, but it has a hard ceiling. As JavaScript in Plain English documents in exhaustive benchmarks, Node.js's single-threaded event loop freezes under CPU-bound tasks, a behavior that surfaces in production voice systems when synchronous work such as TwiML parsing or audio processing blocks the loop, spiking response times from milliseconds to several seconds and causing in-flight calls to time out. Node.js concurrency requires three preconditions that real traffic routinely violates simultaneously.

The same pattern surfaces acutely in concurrent outbound TTS calls: each call triggers webhook round-trips, TwiML parsing, and audio synthesis requests that stack behind one another when volume climbs. Calls queue, then drop. Twilio's free and trial tiers compound this with hard caps on concurrent calls per second, so your infrastructure wall and your provider wall arrive at almost the same moment.

This is the hidden cost that most developers responsible for maintaining existing telephony stacks discover only in production. At 10 calls a day the math is invisible. At high call volumes, well within the limits of a managed voice platform's standard plans, it shows up as pager alerts.

The Multi-Provider Latency Trap#

Each STT-to-TTS-to-telephony handoff adds a new failure surface. DIY TTS call stacks require managing multiple vendors simultaneously, for example, one provider for call orchestration and a separate one for voice synthesis, adding operational complexity that tutorials rarely address and that becomes a genuine burden at scale. Each additional third-party dependency in a chain statistically increases end-to-end failure probability. A 200ms delay at the telephony layer, a 300ms delay at the synthesis layer, and a flaky STT response combine into a call that feels broken to the person on the other end, even when every individual API returned a 200.

Bland.ai collapses that multi-vendor surface into a single per-minute rate, no separate STT vendor contract, no separate TTS API key, no token charges billed on top. Managed platforms typically consolidate those costs into a single per-minute rate that scales with usage, with concurrent call capacity and knowledge base limits that grow across plan tiers. Every plan carries a contractual uptime SLA, so the reliability target is enforceable rather than aspirational.

For teams running continuous outbound campaigns, sales follow-ups, appointment reminders, and inbound customer support intake, removing the multi-vendor latency chain is what makes 24/7 coverage without scaling headcount operationally viable. For organizations already running Amazon Connect, Bland.ai's Amazon Connect Integration means AI voice agents can be substituted for or added alongside human agents inside existing call flows, without migrating to a new platform and without adding yet another vendor credential to manage.

Credential Rot and Webhook Drift#

The maintenance burden grows with call volume. Auth tokens expire. Webhook URLs drift when deployments change.

At scale, a rotated credential triggers a cascade: queued calls fail silently, no retry logic fires, and the incident post-mortem reveals the team spent more time on credential hygiene than on any feature shipped that quarter. Every additional integration added to a DIY stack multiplies this surface, rotating tokens, updating webhook URLs after each deployment, auditing which provider received which call. For developers or IT administrators responsible for the existing stack, this overhead is the hidden tax that makes reducing operational costs tied to customer-facing telephony so difficult to achieve with a patchwork architecture.

Bland.ai's Integrations Platform and conversational pathways, available at a $299/month platform fee, consolidate orchestration so there is no per-vendor webhook surface to maintain separately. Version locking, available on every paid plan, means a deployment does not silently drift the behavior of agents already in production. For organizations with stricter controls, Enterprise adds dedicated infrastructure, SSO, JWT signatures, on-prem/VPC deployment, and compliance documentation available under NDA, with a 30-day deployment framework: scope, build, gray/red/green-team test, and go live with a forward-deployed engineering team.

The operational maintenance burden that compounds with call volume is, in practice, an architecture problem, and Bland Evals shows how systematic quality evaluation at scale, built into the platform, closes the feedback loop that DIY stacks leave entirely to the team to instrument from scratch.

Beyond the Webhook - How Programmable Voice Agents Handle What Generic SDKs Can't#

Owning that stack means owning what comes next. A working webhook feels like infrastructure, but it is a handshake with a provider's edge network, and everything that makes a voice feature production-grade, call-state persistence, concurrent load handling, compliance logging, real-time transcription, sits entirely outside that handshake, waiting for your team to build it.

Our own research found that a single Workbench in Bland Evals can combine up to 10 Eval Agents and run them simultaneously across a selected batch of calls (our data).

Old-way generic SDK gaps versus Bland programmable voice agent capabilities side by side

What a Generic SDK Actually Gives You (and the Long List It Doesn't)#

A telephony SDK gives you authenticated API calls and a documented request format. That's genuinely useful. Conversation state between turns, DTMF branching logic that survives unexpected input, retry handling when a call drops mid-session, and any mechanism to log call outcomes for compliance review are all absent. According to eZintegrations Automation Hub, initial developer time represents a minority of the true project cost for custom integration work. The rest accumulates invisibly: maintenance, security updates, and business-rule changes that never appear in the original estimate.

The Webhook Maintenance Tax - Why Call-Control Logic Compounds Over Time#

Every new production requirement adds a provider. Real-time transcription means a speech-to-text vendor. Sentiment tagging means another API key, another auth rotation, another failure surface. The same eZintegrations research puts ongoing maintenance for a single enterprise integration at $25,000 to $60,000 per year, compounding annually. That number assumes stable APIs. Voice infrastructure is not stable.

Latency, Compliance, and Concurrency as First-Class Concerns, Not Afterthoughts#

Node.js's single-threaded event loop handles async I/O well at low volume. Under concurrent call load, it silently queues or drops requests without surfacing an error. TCPA and GDPR compliance, meanwhile, require documented consent workflows, call recording notices, and auditable retention policies that a generic SDK leaves entirely to the developer to design, build, and maintain. These are not edge cases. They are the operational contract of any production voice system.

How Programmable Voice Agents Replace the DIY Stack With a Single API Call#

Most teams stitch together a telephony provider, a TTS engine, and a speech-to-text layer independently, which means three separate vendor relationships, three credential rotation schedules, and three independent failure surfaces. A programmable voice agent platform collapses that surface into a single API call that handles synthesis, transcription, telephony routing, and conversation state as a unified system, so your team can focus on the call logic that differentiates your product.

Next steps#

If your voice project works on localhost but buckles the moment real call volumes, DTMF branches, and credential rotation enter the picture, the path forward starts with treating the working demo as 10% of the surface area, not the finish line. Start with the best AI phone agent platform for enterprises.

Node.js's single-threaded event loop silently queues and drops calls under concurrent load without surfacing a single error, which means reliability actively degrades at exactly the moment your business needs it most. The TwiML Bin versus Express server choice is not a convenience decision but a disguised architectural commitment that either preserves or forecloses every call-control capability you will want later. Together, those two realities point to the same conclusion: stitching a DIY stack from a telephony provider, a TTS engine, and a speech-to-text layer means owning three credential rotation schedules, three failure surfaces, and an ongoing maintenance bill that industry research puts at $25,000 to $60,000 per year for a single enterprise integration.

Start with bland.ai, the best AI voice platform for enterprise phone calls. From there, conversational pathways, real-time transcription, and concurrency handling are built into the runtime, so your team can focus on the call logic that differentiates your product rather than the infrastructure plumbing the tutorial left out.

Frequently Asked Questions#

How do I set the destination phone number for an outbound TTS call?#

Set the destination number in your `.env` file as `TO_PHONE_NUMBER=+15559876543` using E.164 format, then reference it in `client.calls.create` via `to: process.env.TO_PHONE_NUMBER`. Keeping the number in `.env` rather than hardcoded in your source file prevents accidental exposure if your repository ever goes public.

Why does Twilio show the call as 'queued' instead of confirming it actually rang?#

A `queued` status only means Twilio accepted the request and placed it in its dialing queue, not that the phone rang or the call connected. Actual delivery depends on carrier routing, the recipient's device state, and whether your TwiML URL responds correctly when Twilio fetches it.

How does Node.js handle multiple simultaneous outbound call webhooks without slowing down?#

Node.js handles concurrent I/O without blocking the event loop, which means one slow carrier response doesn't freeze every other in-flight call. This non-blocking architecture is especially valuable in telephony integrations where dozens of simultaneous webhook callbacks, such as Twilio fetching TwiML or delivering DTMF events, can arrive at the same time.

Can I use a TwiML Bin to support multiple languages or personalized messages?#

No, a TwiML Bin is a static hosted XML snippet that cannot read a database, personalize content, or handle conditional logic like language selection. For anything beyond a fixed, never-changing message, you need a dynamic Express server that returns a `VoiceResponse` at runtime, where you can tailor the `language` parameter and message content per caller.

Is it safe to use a public ngrok tunnel for my TwiML webhook endpoint?#

Only if you validate the `X-Twilio-Signature` header on every incoming request. Free ngrok tunnels impose no authentication on inbound traffic by default, so without signature validation your publicly reachable endpoint will accept any POST request, including crafted malicious payloads that could drain your Twilio balance or trigger unintended actions. The free plan also assigns a new random URL every time ngrok restarts, breaking all your Twilio webhook references until you manually update them.

See Bland on your actual call volume.

10 to 15 minutes with the team that ships your first agent. We come prepared with answers, not a pitch deck.

Book a call
Written byEthan ClouserContributor