4 ways to send SMS with Node.js

Send SMS from Node.js with Twilio, Bird, Plivo, or a notification API. Working code for each option, plus how to add templates and automatic provider failover.

Updated Sep 11, 2026

Last updated: September 2026. Package versions verified against npm on 2026-09-11.

SMS is how applications reach users who will not open an app or an inbox: two-factor codes, delivery updates, appointment reminders, fraud alerts, password resets. Node.js has no built-in way to send one, so you pick a provider and call their API.

There are four realistic ways to do it, and they differ less in the send call than in what happens after the first one works.

Which option should you pick?

  • Twilio if you want the shortest path to a working send and the largest body of documentation and community answers to draw on.
  • Bird (formerly MessageBird) if you are already on Bird for email or WhatsApp and want one vendor across channels.
  • Plivo if per-message cost is the deciding factor and you are sending high volume.
  • A notification API in front of any of the three once SMS stops being a one-off script and becomes part of a product flow, because that is the point where templates, user preferences, and failover start costing you real engineering time.

The first three are direct SMS APIs and are interchangeable in shape: install a package, create a client, call a create-message method. The fourth is a different layer, not a fourth vendor, and you generally use it with one of the first three rather than instead of one.

1. Send SMS with Twilio

Twilio's Programmable Messaging API sends and receives SMS, reports delivery status through webhooks, and schedules messages for later. It is the most widely used option in Node.js, which mostly matters when you are debugging something unusual and need a prior art search to turn up results.

What you need first

  • A Twilio account, trial or paid.
  • Node.js 18 or newer.
  • An SMS-enabled Twilio phone number, bought in the Twilio Console.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number. Skipping this is the most common reason a working code sample delivers nothing in production.

Install the helper library

npm install twilio dotenv

Verified against twilio 6.1.1.

Send the message

require('dotenv').config();
// With no arguments, the client reads TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN
// from the environment.
const client = require('twilio')();
const message = await client.messages.create({
body: 'Your order has shipped.',
from: process.env.TWILIO_PHONE_NUMBER,
to: process.env.CELL_PHONE_NUMBER,
});
console.log(message.sid, message.status);

Run it with node sms.js. The returned status is queued or accepted, not delivered. To find out what actually happened to the message you either poll client.messages(sid).fetch() or, better, register a status callback webhook so Twilio posts each status transition to your app.

Where it gets thin: one provider, no template storage, and nothing about user preferences. Delivery status arrives as raw webhooks that you store and interpret yourself.

2. Send SMS with Bird (formerly MessageBird)

MessageBird is now Bird, and this is the part most existing tutorials get wrong. The old messagebird npm package was last published in January 2023 and is no longer the supported path. The current library is @messagebird/sdk, which exposes a BirdClient covering SMS, email, WhatsApp, and verification.

What you need first

  • A Bird account and an API key. The key is region-scoped and looks like bk_{region}_…; the SDK infers which region to call from that prefix, so you do not configure a base URL.
  • Node.js 20.3 or newer. The SDK is ESM and uses fetch, AbortSignal, and Web Crypto rather than Node built-ins, so it also runs on Cloudflare Workers, Vercel Edge, and Deno.
  • A registered sender ID or number.

Install

npm install @messagebird/sdk

Verified against @messagebird/sdk 0.63.0. This one is a moving target: it shipped six minor versions in under two months, and pre-1.0 packages can change shape between minors, so check the types if a call does not match what you see here.

Send the message

import { BirdClient } from '@messagebird/sdk';
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY });
const msg = await bird.sms.send({
from: 'MyBrand',
to: '+14155550100',
text: 'Your verification code is 123456.',
category: 'authentication',
});
console.log(msg.id, msg.status);

Two things differ from the old SDK beyond the names. category is required alongside free text, which is how Bird classifies traffic. And the result is accepted rather than delivered, so you read the message back with bird.sms.get(msg.id) to get a delivery status, segment count, and cost.

Bird throws typed errors rather than using a callback, so you narrow with instanceof:

import { BirdRateLimitError, BirdValidationError } from '@messagebird/sdk';
try {
await bird.sms.send({ from: 'MyBrand', to, text, category: 'authentication' });
} catch (err) {
if (err instanceof BirdRateLimitError) {
console.log(`rate limited, retry in ${err.retryAfter}s`);
} else if (err instanceof BirdValidationError) {
console.error(err.details);
} else {
throw err;
}
}

Where it gets thin: the SDK is at 0.63.0 and its README does not yet document the SMS surface, so you are reading types to find method signatures. Fine for a team already on Bird, harder to recommend as a first SMS provider.

3. Send SMS with Plivo

Plivo covers the same ground as Twilio at generally lower per-message pricing, which is why it shows up in high-volume sending. The tradeoff is a smaller community, so unusual problems take longer to resolve.

What you need first

  • A Plivo account, plus your Auth ID and Auth Token.
  • A Plivo phone number or registered sender ID.
  • Node.js 18 or newer.

Install

npm install plivo

Verified against plivo 4.79.0.

Send the message

const plivo = require('plivo');
// With no arguments, the client reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN
// from the environment.
const client = new plivo.Client();
const response = await client.messages.create({
src: process.env.PLIVO_PHONE_NUMBER,
dst: process.env.CELL_PHONE_NUMBER,
text: 'Your order has shipped.',
});
console.log(response.messageUuid);

Both src and dst need to be in E.164 format. For delivery tracking, set up an endpoint and give Plivo its URL; Plivo posts each status change there as the message progresses.

Where it gets thin: same shape as Twilio, so the same gaps. One provider, no templates, no preference handling.

4. Send SMS with a notification API

The three options above solve delivery. They do not solve everything that shows up in the two weeks after delivery works, which is usually where the actual engineering time goes:

  • The message content is a string literal in your application code, so changing wording means a deploy.
  • A user replies STOP and you now own an opt-out list.
  • The same event needs to reach one user by SMS and another by email.
  • Your provider has a bad hour and there is no second path.
  • Twenty events fire in a minute and the user gets twenty texts.
  • Someone asks how many notifications failed last week and you have no answer.

A notification API such as Courier sits above your SMS provider and handles those. You keep Twilio, Bird, or Plivo as the thing that actually delivers, and your application stops knowing which one that is.

What you get for the extra layer:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes so a burst of events does not become a burst of texts.
  • One send endpoint for SMS, email, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences so opt-outs are enforced before a send rather than after a complaint.
  • Templates edited outside your codebase, with localization per user.
  • One place to see what was sent and what failed, across every channel.

Set it up

Create a free account: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. Then connect an SMS provider under Channels by entering the credentials that provider already gave you. That is the same Twilio or Plivo account from the sections above; you are not changing who delivers your SMS.

Install the SDK

npm install @trycourier/courier

Verified against @trycourier/courier 9.5.0. Requires Node.js 20 or newer, and also runs on Deno, Bun, Cloudflare Workers, and Vercel Edge.

Send the message

import Courier from '@trycourier/courier';
// Reads COURIER_API_KEY from the environment.
const client = new Courier();
const { requestId } = await client.send.message({
message: {
to: { phone_number: '+15558675309' },
content: {
title: 'Order update',
body: 'Hi Erika, your order has shipped.',
},
routing: {
method: 'single',
channels: ['sms'],
},
},
});
console.log(requestId);

That sends content defined inline. The version you will use in production references a template you published in Courier and addresses the user rather than a raw phone number:

const { requestId } = await client.send.message({
message: {
to: { user_id: 'user-123' },
template: 'ORDER_SHIPPED',
data: { name: 'Erika', tracking_url: 'https://example.com/t/abc' },
},
});

Nothing in that call names SMS. Routing and the user's preferences decide the channel, so switching a notification from SMS to push, or sending both, is a configuration change rather than a code change.

Where it gets thin: it is one more service in the path, and one more concept to learn before your first send. It is also not the right tool for two-way SMS conversations, which belong with your provider's inbound APIs.

Building this with an AI agent

If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between Node.js that compiles and Node.js that does not.

The MCP server gives the agent typed tool calls. In Claude Code:

claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY

Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:

npx skills add trycourier/courier-skills

Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.

Then ask in plain language: "send an SMS when an order ships, and fall back to email if it fails." The agent picks the right primitive, and knows which channels need a recorded opt-in.

This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.

Comparing the four options

TwilioBirdPlivoNotification API
Packagetwilio 6.1.1@messagebird/sdk 0.63.0plivo 4.79.0@trycourier/courier 9.5.0
Send callclient.messages.create()bird.sms.send()client.messages.create()client.send.message()
Lines to first send~6~6~6~10
Other channelsSMS, WhatsApp, email, voiceSMS, WhatsApp, emailSMS, WhatsApp, voiceWhatever you connect
Templates outside your codenoSMS templatesnoyes
User preferences and opt-outsyou build ityou build ityou build itbuilt in
Provider failovern/an/an/ayes, across providers
Best forfirst send, widest communityteams already on Birdhigh volume, cost-sensitiveSMS inside a real product flow

The honest summary: if you need one text message sent this afternoon, install twilio and stop reading. If SMS is becoming a feature rather than a script, the thing you will wish you had picked earlier is the layer that lets you change providers, wording, and channels without touching application code.

Next steps


FAQ

Frequently asked questions

The shortest path is npm install twilio, create a client with your Account SID and Auth Token, and call client.messages.create({ body, to, from }). That is about six lines and it will deliver a text message. It gives you a single provider and no templates, preferences, or retry logic, so most teams outgrow it once SMS becomes part of a real product flow.

One API, every channel

Ship notifications without the boilerplate

Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.

Last updated Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.