Send SMS

One API for every text you send.

Set up in:
Cursor

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.

send-otp.ts
200 · 0.4s
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

Hey Ada — your Bird sign-in code is 482917. It'll expire in 10 minutes. Don't share it with anyone.
482917
Delivered

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.

1
2
3
4
5
6
7
8
9
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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

twilio.ts
Twilio
import twilio from "twilio";

const client = twilio(accountSid, authToken);

await client.messages.create({
  from: "+14155550172",
  to:   "+15005550006",
  body: "Your code is 123456.",
});
bird.ts
Bird
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.

segments.ts
200 · 1 segment
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); // → 1

One 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.

reminders.ts
202 · batch
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.

app/api/webhooks/bird/route.ts
signed
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?+
A GSM-7 message fits 160 characters per segment; switching to Unicode (UCS-2) for emoji or non-Latin scripts drops that to 70. Longer messages are concatenated across segments, and the SDK reports the count before you send.
Can I send to many recipients in one request?+
Yes. Batch independent messages in a single call, each with its own recipient and text. The batch validates as a unit, and one idempotency key covers the whole request.
What happens if I retry a send after a timeout?+
Pass an idempotency key and a retried request returns the original result instead of sending twice. Without one, a retry is treated as a new message.
How do I know whether a message was delivered?+
Every state change fires an HMAC-signed webhook — queued, sent, delivered, or failed — carrying the carrier delivery receipt and the segment count.

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.

Start with one channel.
Add the others when you're ready.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

Using Claude Code, Cursor, or Codex? Copy a setup prompt and your agent installs the Bird CLI and skills for you. Pick yours:

Cursor