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

Commencez avec un seul canal.
Ajoutez les autres quand vous êtes prêt.

Une clé API de test est disponible immédiatement. L'accès production se débloque dès que vous ajoutez un moyen de paiement et vérifiez un expéditeur.

CommencerLire la docou

Vous utilisez Claude Code, Cursor ou Codex ? Connectez-le à notre serveur MCP — 141 outils, un par endpoint API, avec des clés d'agent à portée limitée.