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

Mulai dengan satu channel.
Tambahkan yang lain saat Anda siap.

API key uji coba langsung tersedia untuk Anda. Akses produksi terbuka setelah Anda menambahkan metode pembayaran dan memverifikasi pengirim.

Mulai sekarangBaca dokumentasiatau

Menggunakan Claude Code, Cursor, atau Codex? Arahkan ke MCP server kami — 141 tools, satu per API endpoint, dengan scoped agent keys.