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

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.