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
| Channel | Endpoint | SDK |
|---|---|---|
POST /v1/emails | bird.email.send | |
| SMS | POST /v1/sms | bird.sms.send |
| Voice | POST /v1/voice/calls | bird.voice.calls.create |
POST /v1/whatsapp/messages | bird.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 theIdempotency-Keyheader. Same key + same payload within 24h returns the original response.metadata— flatRecord<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
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.
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.