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.

Begin met één kanaal.
Voeg de rest toe wanneer je er klaar voor bent.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra je een betaalmethode toevoegt en een afzender verifieert.

Aan de slagLees de docsof

Gebruik je Claude Code, Cursor of Codex? Verwijs naar onze MCP-server — 141 tools, één per API-endpoint, met scoped agent keys.