Two-way

Texts come back. So handle them.

Set up in:
Cursor

Every message someone sends to your provisioned number arrives as an HMAC-signed webhook. Read the text, reply from the same number, and let Bird handle STOP and HELP for you. Build conversational flows and auto-replies on the API you already send with.

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

Inbound is just another webhook.

Two-way is part of the Bird SMS API. When someone texts your number, you get an sms.received event on the same signed, replay-protected endpoint that already carries your delivery receipts. There's no second integration and no polling — verify the signature once and switch on the type.

Listen for what comes back.

An inbound message and an opt-out are events, same as a delivery receipt. Verify one signature, switch on the type, and handle each one in the handler you already wrote.

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.received":
      await handleInbound(event.data.from, event.data.text);
      break;
    case "sms.opted_out":
      await removeFromCampaigns(event.data.from);
      break;
  }

  return new Response(null, { status: 204 });
}

The payload is the same envelope on every Bird channel: an evt_ id, an HMAC signature, and a replay-protected timestamp.

  • sms.receivedAn inbound message landed on your number — carries the sender, your number, and the text.
  • sms.deliveredA reply you sent reached the handset (carrier DLR).
  • sms.opted_outThe sender texted STOP — Bird suppressed them and blocks future sends.

A signed inbound message, in full.

Here's what an sms.received event looks like on the wire. The from is whoever texted you, the to is your provisioned number, and segments and encoding are reported the same way they are on a send, so a long inbound reply is never a surprise.

sms.received
evt_
{
  "id": "evt_7nQ9xLp2aR...",
  "type": "sms.received",
  "created_at": "2026-06-26T14:03:11Z",
  "data": {
    "id": "sms_5hV02Mr3n...",
    "from": "+15005550006",
    "to": "+14155550172",
    "text": "YES book me in for Thursday",
    "encoding": "GSM-7",
    "segments": 1
  }
}

Reply from the same number.

A reply is a send with from and to swapped. Set from to your number and to to the original sender, and the conversation stays on one number, so the recipient sees a thread instead of a new sender each time. Build auto-replies, confirmations, or a full conversational flow on top.

reply.ts
200 · reply
async function handleInbound(from: string, text: string) {
  if (/^yes\b/i.test(text)) {
    const { error } = await bird.sms.send({
      from: "+14155550172", // your two-way number
      to:   from,           // reply to the sender
      text: "Booked. See you Thursday at 10am.",
    }).safe();

    if (error) throw error;
  }
}

STOP, HELP, and START are handled for you.

Bird recognizes the reserved keywords before they reach your handler: STOP adds the sender to your suppression list and fires sms.opted_out, HELP returns an automatic help reply, and START opts them back in. Later sends to a suppressed number are blocked automatically. You can still keep your own keywords for YES, BOOK, or anything your flow needs. The full keyword and opt-out rules live in opt-out handling.

Two things you'll want next.

Inbound needs a two-way-capable number — long codes, short codes, and toll-free can receive, alphanumeric sender IDs can't. For richer back-and-forth on the same handset (typing indicators, read receipts, carousels), RCS upgrades the conversation where the device supports it.

Go deeper in the docs.

Wire up webhooks for inbound events, read the error reference for the failures you'll handle, and check abuse and compliance for the keyword and consent rules.

Two-way SMS FAQ

How does an inbound SMS reach my app?+
Every message sent to your provisioned number arrives as an HMAC-signed sms.received webhook. You verify one signature, read the from, to, and text, and route it into your own logic — the same envelope you already handle for delivery receipts.
Can I reply to an inbound message?+
Yes. Reply by sending from the same number the message came in on. Set from to your number and to to the original sender, and the thread stays on one number end to end.
What happens when someone texts STOP?+
Bird adds the sender to your suppression list and fires sms.opted_out, and blocks later sends to that number automatically. HELP returns a help reply and START opts them back in — all handled before they reach your code, so you stay compliant without writing the keyword logic yourself.
Do I need a special number for two-way?+
You need a two-way-capable number. Long codes, short codes, and toll-free numbers support inbound; alphanumeric sender IDs are send-only and can't receive replies.

Send and receive on one number, one API.

Two-way inbound is one capability of the Bird SMS API: sending, numbers, 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