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

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.