Migrate from Twilio to Bird.

A six-section playbook: concept map, OTP code diff, webhook signature verifier in both shapes, a 7-day dual-write cutover, an 8-step checklist, and an honest list of the four Twilio products that don't have a Bird equivalent today.

Twilio resources to Bird resources.

Most calls translate one-for-one. A handful are partial overlaps — flagged in the right-most column.

TwilioBirdNote
twilio.messages.create({ to, from, body })bird.sms.send({ to, from, text })Same shape. Bird returns { data, error }.
twilio.verify.v2.services(sid).verifications.create({ to, channel })bird.verifications.start({ to, channel, fallback: 'voice' })Fallback is a single attribute on Bird.
twilio.verify.v2.services(sid).verificationChecks.create({ to, code })bird.verifications.check({ id, code })Bird checks by verification id, not phone.
twilio.calls.create({ to, from, url })bird.voice.calls.create({ to, from, twiml | url })TwiML is accepted at the URL.
twilio.lookups.v2.phoneNumbers(num).fetch({ fields })bird.lookup.get({ phone })Single call returns line type, carrier, ported-from, fraud score.
twilio.notify.v1.services(sid).notifications.create(...)bird.push.send({ to, title, body })Partial overlap only — Notify is broader than Bird Push today.
Twilio Conversationsno Bird equivalent yetHonest gap. See the bottom-of-page section.

The OTP send-and-verify flow, both shapes.

Twilio Verify on the left. Bird Verifications on the right. Same recipient, same code, same outcome — the path collapses from twenty-eight lines to eleven.

twilio-otp.ts
before
import twilio from "twilio";

const client = twilio(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_AUTH_TOKEN,
);

// 1. Start a verification.
const verification = await client.verify.v2
  .services(process.env.TWILIO_VERIFY_SID!)
  .verifications.create({
    to:      "+14155550172",
    channel: "sms",
  });

// At this point Twilio has sent a code.
// verification.sid is the handle.

// 2. Check the code the user typed.
const check = await client.verify.v2
  .services(process.env.TWILIO_VERIFY_SID!)
  .verificationChecks.create({
    to:   "+14155550172",
    code: userInputCode,
  });

if (check.status !== "approved") {
  throw new Error("Verification failed");
}

console.log("OK:", check.sid);
// → "VE9a8b7c6d5e4f..."
bird-otp.ts
after
import { BirdClient } from "@messagebird/sdk";

const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

// 1. Start a verification.
const { data: v } = await bird.verifications.start({
  to:       "+14155550172",
  channel:  "sms",
  fallback: "voice",
});

// 2. Check the code the user typed.
const { data: result, error } = await bird.verifications.check({
  id:   v.id,
  code: userInputCode,
});

if (error) throw error;
console.log("OK:", result.id);
// → "ver_3nB91x..."

Different header, different hash, different signed surface.

Twilio signs the URL plus sorted form-body params with HMAC-SHA1 and base64-encodes the result. Bird signs the timestamp plus the raw JSON body with HMAC-SHA256 and hex-encodes it. The Bird header is Bird-Signature: t=<ts>,v1=<hex> — the timestamp is what protects against replay.

twilio-webhook.ts
SHA-1 · URL + body
import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post("/webhooks/twilio", (req, res) => {
  const signature = req.header("X-Twilio-Signature") ?? "";
  const url       = "https://api.example.com/webhooks/twilio";

  // Twilio signs the URL + the sorted form-body params, HMAC-SHA1, base64.
  const sorted = Object.keys(req.body)
    .sort()
    .map((k) => k + req.body[k])
    .join("");

  const expected = crypto
    .createHmac("sha1", process.env.TWILIO_AUTH_TOKEN!)
    .update(url + sorted)
    .digest("base64");

  if (signature !== expected) {
    return res.status(403).end();
  }
  res.status(204).end();
});
bird-webhook.ts
SHA-256 · ts + body
import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/bird", (req, res) => {
  const header = req.header("Bird-Signature") ?? "";
  // Header shape: "t=1716200000,v1=abcd..."
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string]),
  );

  // Bird signs the timestamp + raw body, HMAC-SHA256, hex.
  const signed = parts.t + "." + (req.body as Buffer).toString("utf8");
  const expected = crypto
    .createHmac("sha256", process.env.BIRD_WEBHOOK_SECRET!)
    .update(signed)
    .digest("hex");

  if (parts.v1 !== expected) {
    return res.status(403).end();
  }
  res.status(204).end();
});

Dispatch to both vendors for seven days, then flip the canary.

For the first week of the cutover, every send goes to Twilio (the primary) and Bird (the mirror) in parallel. The Bird dashboard surfaces per-route delivery rates — compare them by country, carrier, and route type before promoting Bird to primary. The mirror is non-blocking, so a Bird error never breaks the live path.

send-sms.ts
dual-write
// Days 1-7 of the cutover: dispatch every send to BOTH vendors.
// Compare per-route delivery rates on the Bird dashboard before flipping
// production traffic.
import twilio from "twilio";
import { BirdClient } from "@messagebird/sdk";

const tw = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });

export async function sendSms(to: string, body: string) {
  // Production traffic still goes through Twilio.
  const primary = tw.messages.create({ to, from: "+14155550100", body });

  // Bird gets a mirrored send, tagged for the comparison report.
  const mirror = bird.sms.send({
    to,
    from: "+14155550100",
    text: body,
    tags: { migration_cohort: "dual-write" },
  });

  // Don't block on the mirror if it errors.
  const [primaryResult] = await Promise.all([
    primary,
    mirror.catch((e) => ({ data: null, error: e })),
  ]);

  return primaryResult;
}

Eight steps from first key to retired Twilio token.

Run them in order. Each step has a rollback. The total elapsed time on a typical migration is 14–21 days, dominated by the 72-hour dual-write hold.

  1. 01Provision a Bird production API key, scoped per service.
  2. 02Verify your sender domain (email) and at least one alphanumeric sender or long code (SMS).
  3. 03Port webhook handlers: add the Bird-Signature verifier alongside the X-Twilio-Signature one.
  4. 04Deploy dual-write to staging. Validate that both providers return success for the OTP, SMS, and voice paths.
  5. 05Promote dual-write to production. Watch per-route delivery rates on the Bird dashboard for at least 72 hours.
  6. 06Flip the canary: route 10% of production sends Bird-primary, Twilio-mirror. Hold for 24h.
  7. 07Ramp to 100% Bird-primary. Keep Twilio mirror enabled for 7 more days as the rollback path.
  8. 08Disable the Twilio mirror, rotate the Twilio auth token to a parking value, and archive the old code path.

Four Twilio products with no Bird equivalent today.

We'd rather you know on day zero than find out in week four. If your Twilio usage is heavy on any of these four, the migration is partial and you should keep that vendor relationship open.

  • Twilio ConversationsStateful multi-party chat over SMS, MMS, WhatsApp, and chat. Bird routes the underlying channels but does not maintain conversation state for you today. Roll your own state on top of bird.sms and bird.whatsapp, or stay on Conversations for this surface.
  • Twilio StudioDrag-and-drop visual flow builder. No Bird-built equivalent today. Teams using Studio for non-engineering-owned flows typically keep it standalone and add Bird underneath for the messaging volume.
  • Twilio FrontlineSales/CS frontend app for one-to-one messaging. No Bird-built equivalent. Most teams using Frontline keep it standalone and add Bird underneath for messaging volume on other surfaces.
  • Twilio TaskRouterSkills-based routing engine for contact centers. Not in the Bird API surface today. Customers on this typically pair Bird messaging with a dedicated CCaaS for queueing and routing.

Cutover week is staffed.

Email migrations@bird.com with your Twilio account region, monthly send volume, and the products you use. We'll provision a shared channel and a named engineer for the dual-write window.

Inizia con un canale.
Aggiungi gli altri quando sei pronto.

Una chiave API di test è subito tua. La produzione si sblocca quando aggiungi un metodo di pagamento e verifichi un mittente.

Inizia oraLeggi la documentazioneo

Using Claude Code, Cursor, or Codex? Point it at our hosted MCP server: curated Bird tools, a browser sign-in, and no API key. Or install the bird-ai plugin.

Cursor