Send one message or a hundred through the same SMS API. The SDK counts segments before send, picks GSM-7 or Unicode for you, and every send is idempotent with a webhook on each delivery state.
import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });
const code = generateOtp();
const { data, error } = await bird.sms.send({
from: "Bird",
to: "+15005550006",
text: `Your Bird verification code is ${code}. Reply STOP to opt out.`,
}).safe();
if (error) throw error;
console.log(data.id);
// → "sms_4kT01Lq2m..."Today at 2:14 PM
Send your first SMS in five minutes.
From the language you already use.
Sending is the core of the Bird SMS API. The first send goes to a sanctioned test recipient (+15005550006), so you can ship a CI check and wire up webhooks before you provision a number.
import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });
const { data, error } = await bird.sms.send({
from: "Bird",
to: "+15005550006",
text: "Hello from Node.",
}).safe();Five things you don't build yourself.
The same contract on every Bird channel.
- 01
Segment counting before send.
The SDK measures the encoded length and tells you how many segments a message costs, so a stray character never silently splits one text into three.
- 02
GSM-7 and Unicode, decided for you.
Plain text rides GSM-7; an emoji or non-Latin script flips the whole message to UCS-2. Bird picks the encoding and warns you when a single character changes the cost.
- 03
Batch in one call.
Send many independent messages in one request, each with its own recipient and text, validated as a unit so you never half-send.
- 04
Idempotent by contract.
Every send accepts an idempotency key, so a retried request after a timeout returns the original result instead of texting someone twice.
- 05
A webhook on every state change.
Queued, sent, delivered, failed. Each one HMAC-signed, replay-protected, idempotent, the same envelope on every channel.
Already sending somewhere else? Switch the client, keep the call.
The shape barely changes: swap the client, keep your from, to, and text, point your webhooks at one endpoint. Same auth model as your email, voice, and WhatsApp sends.
import twilio from "twilio";
const client = twilio(accountSid, authToken);
await client.messages.create({
from: "+14155550172",
to: "+15005550006",
body: "Your code is 123456.",
});import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });
await bird.sms.send({
from: "Bird",
to: "+15005550006",
text: "Your code is 123456.",
});Know the cost before the carrier does.
A GSM-7 message fits 160 characters per segment; a single emoji or non-Latin character flips the whole message to UCS-2 and drops that to 70. The SDK reports the encoding and segment count on every send, so billing is never a surprise and a concatenated message is always a deliberate choice.
const { data } = await bird.sms.send({
from: "Bird",
to: "+15005550006",
text: "Your code is 123456.",
}).safe();
console.log(data.encoding); // → "GSM-7"
console.log(data.segments); // → 1One message or a hundred, one call.
Batch independent messages in one request, each with its own recipient and text. The batch validates as a unit: one bad number rejects the call with a 422, so you never half-send. A single idempotency key makes the whole request safe to retry.
const { data: batch, error } = await bird.sms
.sendBatch(
users.map((u) => ({
from: "Bird",
to: u.phone,
text: `Hi ${u.name}, your appointment is tomorrow at ${u.time}.`,
})),
{ idempotencyKey: `reminders-${runId}` },
)
.safe();
if (error) throw error;
console.log(`queued ${batch.data.length} messages`);Watch every message through its whole life.
A send returns 202 immediately; the outcome arrives as a webhook. Verify one signature, switch on the type: the same envelope you already handle for email, voice, and WhatsApp.
import { bird } from "@/lib/bird";
export async function POST(req: Request) {
const event = bird.webhooks.unwrap(
await req.text(),
Object.fromEntries(req.headers),
);
switch (event.type) {
case "sms.delivered":
await markDelivered(event.data.sms_id);
break;
case "sms.failed":
await flag(event.data.to, event.data.reason);
break;
}
return new Response(null, { status: 204 });
}Failed sends and STOP replies update your suppression list automatically, so a bad number never costs you twice.
sms.queuedAccepted by the API and queued for the carrier hand-off.sms.sentSubmitted to the destination carrier's SMSC.sms.deliveredDelivery receipt received from the carrier (DLR).sms.failedPermanent failure — carrier rejection, invalid number, or suppression hit.
Go deeper in the docs.
Wire up webhooks, make every send safe to retry with idempotency keys, and read the error reference so you handle each failure the right way.
SMS sending FAQ
How long can an SMS be?+
Can I send to many recipients in one request?+
What happens if I retry a send after a timeout?+
How do I know whether a message was delivered?+
The rest of the SMS platform
One API, one set of keys. Explore the other capabilities.
About 40% of the world's commercial SMS already runs on Bird.
Sending is one capability of the Bird SMS API: numbers, two-way inbound, compliance, routing, and analytics ship with it, on infrastructure we've run for a decade.