Send a message

Every Bird channel exposes one POST endpoint that sends one message. Email, SMS, voice, and WhatsApp each have their own sub-client on the Bird instance — but the request shape, the auth, the idempotency contract, the response envelope, and the error type are the same on all four.

Per-channel endpoints

ChannelEndpointSDK
EmailPOST /v1/emailsbird.email.send
SMSPOST /v1/smsbird.sms.send
VoicePOST /v1/voice/callsbird.voice.calls.create
WhatsAppPOST /v1/whatsapp/messagesbird.whatsapp.send

No channel parameter on a unified send(). The method you call is the channel.

Common request fields

Every send accepts the same five top-level fields, regardless of channel:

  • to — the recipient. Email address for email, E.164 phone number for SMS / voice / WhatsApp. Arrays are accepted on the channels that support fan-out (email up to 50; SMS, voice, and WhatsApp single-recipient per request).
  • from — verified sender on your account. Email domain, registered number, alpha tag, or pool name.
  • idempotency_key — string you control, also accepted as the Idempotency-Key header. Same key + same payload within 24h returns the original response.
  • metadata — flat Record<string, string> you get back on the response and on every webhook for this message. Use it to thread your own IDs through.
  • webhook_url — per-send override of your account webhook. Useful for one-off routing without re-subscribing.

Beyond these five, each channel has its own body shape — covered below.

Per-channel request schema

Email

const { data, error } = await bird.email.send({
  to: ["delivered@bird.dev"],
  from: "Bird <hello@bird.dev>",
  subject: "Your invite is ready",
  html: "<p>Welcome aboard.</p>",
  text: "Welcome aboard.",
  metadata: { user_id: "usr_2bX9z" },
});

Pass template + variables to render a stored template instead of inline subject/html/text. Pass react with a React Email component to render server-side in the SDK.

SMS

const { data, error } = await bird.sms.send({
  to: "+15005550006",
  from: "+18885550101",
  text: "Your code is 482917. Don't share it.",
  metadata: { user_id: "usr_2bX9z" },
});

from can be a single E.164 number, an alpha tag where the destination supports it, or a sender pool — from: "pool:welcome-sms". MMS via the media array (US/Canada on most routes).

Voice

const { data, error } = await bird.voice.calls.create({
  to: "+15005550010",
  from: "+18885550101",
  flow: [
    { say: "Please enter your account number, followed by the pound sign." },
    { gather: { digits: { min: 6, max: 12, terminator: "#" }, into: "account" } },
    { say: "Thank you." },
  ],
  metadata: { user_id: "usr_2bX9z" },
});

Either a declarative flow (steps array) or a url that Bird fetches per call event.

WhatsApp

const { data, error } = await bird.whatsapp.send({
  to: "+15005550009",
  from: "+18885550101",
  template: "order_confirmed",
  variables: { name: "Ada", order_id: "A-2491" },
  fallback: "sms",
  metadata: { user_id: "usr_2bX9z" },
});

template is the default for any send outside the 24-hour session window. Free-form text, media, and interactive payloads are accepted inside an open session — the response carries session_state: "open" | "closed" so you can branch on it. fallback: "sms" re-routes the same message body to SMS in one request when WhatsApp delivery fails on session expiry.

Common response fields

Every send returns the same envelope:

{
  "id": "email_2bX9z4kP7nQwLmH1jR3vT8aBcDfG",
  "status": "queued",
  "to": "delivered@bird.dev",
  "from": "Bird <hello@bird.dev>",
  "estimated_cost": {
    "amount": "0.00150",
    "currency": "USD"
  },
  "metadata": { "user_id": "usr_2bX9z" },
  "created_at": "2026-05-19T14:32:08.421Z"
}

Typed ID prefixes by channel — email_*, sms_*, call_*, wa_msg_*. status is queued on the synchronous response; state machines progress through webhooks (email.delivered, sms.failed, call.completed, whatsapp.read).

Verifications

For OTP — SMS, voice, email, with retry, expiry, and check semantics handled for you — use the verifications resource rather than building it on top of bird.sms.send.

const { data: started, error: startError } = await bird.verifications.start({
  to: "+15005550006",
  channel: "sms",
  fallback: "voice",
});

const { data: checked, error: checkError } = await bird.verifications.check({
  id: started.id,
  code: "482917",
});

fallback: "voice" falls back to a voice call if SMS delivery degrades in the recipient's region. The verification code itself is never returned to you — it's generated, delivered, and checked entirely on Bird's side. See verifications for TTL, attempt counters, fraud scoring, and the full webhook list.

Streaming TTS

For browser-side or telephony-side playback where first-byte latency matters, stream the audio rather than waiting for the synchronous synthesize response.

const { data: stream, error } = await bird.tts.stream({
  text: "Your verification code is 482917.",
  voice: "en-US-amy",
});

for await (const chunk of stream) {
  // chunk is a Uint8Array of MP3 / Opus / PCM bytes
  audio.write(chunk);
}

First audio byte arrives in under 250ms. HTTP chunked is the default; a WebSocket alternative at wss://api.bird.dev/v1/tts/stream is available when you want to push text into an open synthesis session token-by-token (useful when the upstream text is itself streaming from an LLM).

Idempotency

Every POST accepts an Idempotency-Key header — same key + same payload within 24h returns the cached response without re-sending. Same key + different payload returns 409 idempotency_conflict with the diff in conflicting_fields.

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.

Usi Claude Code, Cursor o Codex? Puntali al nostro server MCP — 141 strumenti, uno per endpoint API, con chiavi agent a scope limitato.