Quickstart · Bun

Send your first email from a pure TypeScript server on Bun.serve. No framework — Bun's native HTTP and crypto are enough.

Prerequisites

  • Bun 1.1 or later
  • A Bird test API key from the dashboardbird_test_xxxxxxxx

Install the SDK

bun add @bird/sdk

Set the env vars

Create .env at the project root:

BIRD_API_KEY=bird_test_xxxxxxxx
BIRD_WEBHOOK_SECRET=whsec_xxxxxxxx

Send + receive in one file

Create server.ts:

import { BirdClient } from "@bird/sdk";

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

function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let mismatch = 0;
  for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return mismatch === 0;
}

Bun.serve({
  port: 3000,
  routes: {
    "/api/send-welcome": {
      POST: async (req) => {
        const { to } = (await req.json()) as { to: string };
        const { data, error } = await bird.email.send({
          from: "Bird <onboarding@bird.dev>",
          to: [to],
          subject: "Welcome",
          html: "<p>It works.</p>",
        });
        if (error) return Response.json({ error }, { status: 500 });
        return Response.json({ id: data.id }, { status: 202 });
      },
    },
    "/webhooks/bird": {
      POST: async (req) => {
        const raw = await req.text();
        const header = req.headers.get("bird-signature") ?? "";
        const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));

        const age = Math.abs(Date.now() / 1000 - parseInt(parts.t ?? "0", 10));
        if (!parts.t || !parts.v1 || age > 300) {
          return Response.json({ error: "invalid_signature" }, { status: 401 });
        }

        const hasher = new Bun.CryptoHasher("sha256", process.env.BIRD_WEBHOOK_SECRET!);
        const expected = hasher.update(`${parts.t}.${raw}`).digest("hex");

        if (!timingSafeEqual(parts.v1, expected)) {
          return Response.json({ error: "invalid_signature" }, { status: 401 });
        }

        const event = JSON.parse(raw) as { type: string; data: { id: string } };
        // event.type === 'email.delivered' | 'email.bounced' | ...
        return Response.json({ received: true }, { status: 200 });
      },
    },
  },
});

console.log("listening on http://localhost:3000");

Run it:

bun --env-file=.env run server.ts
curl -X POST http://localhost:3000/api/send-welcome \
  -H 'content-type: application/json' \
  -d '{"to":"delivered@bird.dev"}'

What just happened

delivered@bird.dev is a sanctioned test recipient — it accepts the send, returns an email_* id, and emits the delivery event back to your webhook. The handler compares HMAC-SHA256 digests in constant time so a forged request never reaches the parser.

Next steps

Commencez avec un seul canal.
Ajoutez les autres quand vous êtes prêt.

Une clé API de test est disponible immédiatement. L'accès production se débloque dès que vous ajoutez un moyen de paiement et vérifiez un expéditeur.

CommencerLire la docou

Vous utilisez Claude Code, Cursor ou Codex ? Connectez-le à notre serveur MCP — 141 outils, un par endpoint API, avec des clés d'agent à portée limitée.