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

Starten Sie mit einem Kanal.
Fügen Sie die anderen hinzu, wenn Sie bereit sind.

Ein Test-API-Key steht Ihnen sofort zur Verfügung. Der Produktivzugang wird freigeschaltet, sobald Sie eine Zahlungsmethode hinzufügen und einen Absender verifizieren.

Jetzt startenDokumentation lesenoder

Sie nutzen Claude Code, Cursor oder Codex? Verbinden Sie es mit unserem MCP-Server – 141 Tools, eines pro API-Endpunkt, mit eingeschränkten Agent-Keys.