Send email

One API for every email you send.

Set up in:

Transactional or marketing, one message or a hundred, sent through the same Email API, with idempotency, suppression, and webhooks built in. Pass raw HTML or render your React Email templates.

welcome.tsx
200 · 1.2s
import { BirdClient } from "@messagebird/sdk";
import { render } from "@react-email/render";
import { WelcomeEmail } from "./emails/welcome";

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

const { data, error } = await bird.email.send({
  from:    "Bird <hello@bird.com>",
  to:      ["ada@example.com"],
  subject: "Your invite is ready",
  html:    await render(<WelcomeEmail name="Ada" />),
}).safe();

if (error) throw error;
console.log(data.id);
// → "em_2bX91Yk8h..."

Send your first email in five minutes.

From the language you already use.

Sending is the core of the Bird Email API. The first send can go to a sanctioned test address (delivered@messagebird.dev), so you can try the whole platform (sends, webhooks, suppression) before you verify a domain.

1
2
3
4
5
6
7
8
9
10
import { BirdClient } from "@messagebird/sdk";

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

const { data, error } = await bird.email.send({
  from:    "you@yourdomain.com",
  to:      ["delivered@bird.dev"],
  subject: "Hello from Node",
  html:    "<p>It works.</p>",
}).safe();

Five things you don't build yourself.

The same contract on every Bird channel.

  1. 01

    Transactional + marketing.

    The same endpoint sends a password reset or a campaign. A category field decides how suppression and unsubscribes apply.

  2. 02

    Templates your way.

    Pass raw HTML, or render React Email templates to HTML in your app and send the result. Your toolchain, unchanged.

  3. 03

    Batch up to 100.

    Up to 100 independent messages per call, each with its own recipient and variables, validated as one unit so you never half-send.

  4. 04

    Idempotent by contract.

    Every send accepts an idempotency key, so a retried request after a timeout returns the original result instead of double-sending.

  5. 05

    A webhook on every state change.

    Accepted, delivered, opened, clicked, bounced, complained. Each one HMAC-signed, replay-protected, idempotent, the same envelope on every channel.

Already sending somewhere else? Switch in an afternoon.

The call you already make barely changes: swap the client, keep your templates, point your webhooks at one endpoint. Migration guides cover SendGrid, Amazon SES, Mailgun, and Resend.

sendgrid.ts
SendGrid
import sgMail from "@sendgrid/mail";

sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

await sgMail.send({
  from:    "hello@bird.com",
  to:      "ada@example.com",
  subject: "Your invite is ready",
  html:    "<p>Welcome aboard, Ada.</p>",
});
bird.ts
Bird
import { BirdClient } from "@messagebird/sdk";

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

await bird.email.send({
  from:    "hello@bird.com",
  to:      ["ada@example.com"],
  subject: "Your invite is ready",
  html:    "<p>Welcome aboard, Ada.</p>",
});

One message or a hundred, one call.

Batch up to 100 independent messages in one request, each with its own recipient and variables. The batch validates as a unit: one bad message rejects the call with a 422, so you never half-send. A single idempotency key makes the whole request safe to retry.

digest.ts
202 · batch
import { BirdClient } from "@messagebird/sdk";
import { render } from "@react-email/render";
import { Digest } from "./emails/digest";

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

const messages = await Promise.all(
  users.map(async (u) => ({
    from:    "Bird <hello@bird.com>",
    to:      [u.email],
    subject: "Your weekly digest",
    html:    await render(<Digest user={u} />),
  })),
);

const { data: batch, error } = await bird.email
  .sendBatch(messages, { idempotencyKey: `digest-${runId}` })
  .safe();

if (error) throw error;
console.log(`queued ${batch.data.length} messages`);

Attach your own context to every send.

Tags are a first-class, filterable dimension: slice delivery and engagement by campaign, template, or experiment in the stats API (up to 20 per message). Metadata is arbitrary JSON, up to 2 KB, that round-trips untouched on every read and webhook, so your own IDs ride along with the message.

tagged.ts
await bird.email.send({
  from:     "Bird <hello@bird.com>",
  to:       ["ada@example.com"],
  subject:  "Your invite is ready",
  html:     "<p>Welcome aboard, Ada.</p>",
  tags:     [{ name: "campaign", value: "spring-2026" }],
  metadata: { user_id: "u_2bX91", order_id: "ord_5512" },
});

Watch every message through its whole life.

A send returns 202 immediately; the outcome arrives as a webhook per recipient. Verify one signature, switch on the type: the same envelope you already handle for SMS, voice, and WhatsApp.

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 "email.delivered":
      await markDelivered(event.data.email_id);
      break;
    case "email.bounced":
      await flag(event.data.recipient, event.data.bounce_type);
      break;
  }

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

Hard bounces, complaints, and unsubscribes also update your suppression list automatically, so a bad address never costs you reputation twice.

  • email.acceptedAccepted by the API and queued for delivery.
  • email.processedGenerated and handed to the sending pipeline.
  • email.deliveredThe receiving mail server accepted the message.
  • email.deferredA transient failure that Bird retries automatically.
  • email.bouncedPermanent failure: bounce type and SMTP code in the payload.
  • email.openedTracking pixel loaded (non-prefetched opens count).
  • email.clickedA tracked link was clicked.
  • email.complainedThe recipient reported the message as spam.
  • email.unsubscribedThe recipient unsubscribed via a tracked link in the message body.

Test every outcome before you go live.

In the sandbox, the address decides the result, not your account state. Send to delivered@messagebird.dev for a clean delivery, or to bounce@, complaint@, and suppressed@ to drive each failure path through the real pipeline and webhooks. No domain to verify, no risk to your reputation. Production sending is gated the way it should be: you verify a domain first, and a new domain or dedicated IP ramps through reputation warmup before it carries full volume.

Go deeper in the docs.

Read the sending guide, wire up email events and webhooks, or, if you're coming from another provider, follow a migration guide from SendGrid, SES, Mailgun, or Resend.

Sending FAQ

Can I send both transactional and marketing email?+
Yes. Both go through the same send API; the only difference is the category field, which decides how suppression and unsubscribes are applied. Pick transactional for password resets and receipts, marketing for campaigns.
Do you support React Email templates?+
Render your React Email templates to HTML in your application — the render function from @react-email/render works unchanged — and pass the result as the html body. Nothing in your template toolchain changes.
How many emails can I send in one request?+
Up to 100 independent messages per batch call, each with its own recipient, subject, and variables. The batch is validated as a unit, so one invalid message rejects the whole call with a 422 — you never half-send. A single message takes 1–50 recipients across to, cc, and bcc.
What happens if a request times out and I retry it?+
Send an Idempotency-Key with each logical send. If the original request succeeded but the response was lost, replaying with the same key returns the original result (marked with an Idempotency-Replay header) instead of sending again.
What are the rate limits?+
Single sends and batches have separate limits — on the free tier, 10 single sends and 5 batch calls per minute (5 × 100 = 500 messages/min), rising to 1,000 sends and 100 batch calls per minute on Growth. Limits count requests, not recipients, so batching is the volume lever.
How do I test without sending real email?+
Send to a sandbox address such as delivered@messagebird.dev — the outcome is decided by the address, not your account state — so you can exercise sends, webhooks, and suppression before you verify a domain. Addresses like bounce@ and complaint@ simulate those outcomes through the real pipeline.

About 40% of the world's commercial email already runs on Bird.

Transactional and marketing email on infrastructure we've run for a decade. Sending is one capability of the Bird Email API: deliverability, dedicated IPs, suppression, and analytics ship with it.

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: