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-notification.ts
202 · 0.4s
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:     "Your order #4821 has shipped. Track it: bird.ly/t/4821x",
  category: "transactional",
}).safe();

if (error) throw error;
console.log(data.id);
// → "sms_4kT01Lq2m..."
Hi Ada, reminder of your appointment tomorrow at 14:30 with Dr. Kowalski.
Your order #4821 has shipped. Track it: bird.ly/t/4821x
Your subscription renews on 3 Sep for €12/mo. Manage it here: bird.ly/account

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
const msg = await bird.sms.send({
  from: "+15557654321",
  to: "+14155550100",
  text: "Your verification code is 123456.",
  category: "authentication",
});
console.log(msg.id, msg.status);

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.",
  category: "authentication",
});

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.",
  category: "authentication",
}).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.acceptedAccepted 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.

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