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.
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.
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
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.
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
bk_{region}_…; the SDK infers which region to call from that prefix, so you do not configure a base URL.fetch, AbortSignal, and Web Crypto rather than Node built-ins, so it also runs on Cloudflare Workers, Vercel Edge, and Deno.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.
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
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.
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:
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:
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.
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.
| Twilio | Bird | Plivo | Notification API | |
|---|---|---|---|---|
| Package | twilio 6.1.1 | @messagebird/sdk 0.63.0 | plivo 4.79.0 | @trycourier/courier 9.5.0 |
| Send call | client.messages.create() | bird.sms.send() | client.messages.create() | client.send.message() |
| Lines to first send | ~6 | ~6 | ~6 | ~10 |
| Other channels | SMS, WhatsApp, email, voice | SMS, WhatsApp, email | SMS, WhatsApp, voice | Whatever you connect |
| Templates outside your code | no | SMS templates | no | yes |
| User preferences and opt-outs | you build it | you build it | you build it | built in |
| Provider failover | n/a | n/a | n/a | yes, across providers |
| Best for | first send, widest community | teams already on Bird | high volume, cost-sensitive | SMS 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.
FAQ
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.messagebird package on npm was last published in January 2023 at version 4.0.1. MessageBird is now Bird, and the current library is @messagebird/sdk, which uses a BirdClient and a bird.sms.send() call instead of the old messagebird.messages.create(). Any Node.js tutorial still showing require('messagebird') is pointing at an abandoned package.@trycourier/courier, set COURIER_API_KEY in your environment, and construct the client with new Courier(). Then call client.send.message() with a message object naming the recipient by user_id or phone_number, either inline content or a published template, and optional routing. The call returns a requestId you can use to look up delivery status.user_id, and the platform resolves which of SMS, email, push, or in-app to use based on the notification's routing and that user's preferences. Adding a channel later becomes a configuration change instead of a code change.Keep exploring
One API, every channel
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.