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 dashboard —
bird_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
- Verify a sending domain — graduate from test keys to production sends.
- Send an SMS — same auth, same response envelope.
- Webhooks deep-dive — the full event catalog and retry schedule.
- Drop the MCP server in your IDE — let Claude and Cursor send through Bird.