Quickstart · Express

Send your first email from an Express 5 + TypeScript server. One POST route to send, one to receive the delivery webhook.

Prerequisites

  • Node 20 or later
  • pnpm 9 or later
  • A Bird test API key from the dashboardbird_test_xxxxxxxx

Install the SDK

pnpm add express @bird/sdk
pnpm add -D typescript tsx @types/express @types/node
# npm install express @bird/sdk
# yarn add express @bird/sdk
# bun add express @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 src/server.ts:

import express, { Request, Response } from "express";
import { BirdClient } from "@bird/sdk";
import crypto from "node:crypto";

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

app.use("/webhooks/bird", express.raw({ type: "application/json" }));
app.use(express.json());

app.post("/api/send-welcome", async (req: Request, res: Response) => {
  const { to } = req.body 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 res.status(500).json({ error });
  return res.status(202).json({ id: data.id });
});

app.post("/webhooks/bird", (req: Request, res: Response) => {
  const header = String(req.headers["bird-signature"] ?? "");
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const raw = req.body as Buffer;

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

  const expected = crypto
    .createHmac("sha256", process.env.BIRD_WEBHOOK_SECRET!)
    .update(`${parts.t}.${raw.toString("utf8")}`)
    .digest("hex");

  const sigBuf = Buffer.from(parts.v1, "hex");
  const expBuf = Buffer.from(expected, "hex");
  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
    return res.status(401).json({ error: "invalid_signature" });
  }

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

app.listen(3000, () => console.log("listening on http://localhost:3000"));

Run it:

pnpm tsx src/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 every send and emits the full event sequence without touching a real inbox. The webhook handler verifies the Bird-Signature header with HMAC-SHA256 and crypto.timingSafeEqual before parsing the body, so a forged request never reaches your business logic.

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.