Quickstart · Python

Send your first email from a FastAPI app. Async client for the send, Pydantic for the webhook payload.

Prerequisites

  • Python 3.11 or later
  • A Bird test API key from the dashboardbird_test_xxxxxxxx

Install the SDK

pip install bird-sdk "fastapi[standard]"
# uv add bird-sdk "fastapi[standard]"
# poetry add bird-sdk "fastapi[standard]"

Set the env vars

Export them in your shell, or use a .env loader:

export BIRD_API_KEY=bird_test_xxxxxxxx
export BIRD_WEBHOOK_SECRET=whsec_xxxxxxxx

Send + receive in one file

Create main.py:

import hmac
import hashlib
import os
import time
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from bird import Bird

bird = Bird(api_key=os.environ["BIRD_API_KEY"])
app = FastAPI()


class SendRequest(BaseModel):
    to: str


class EventData(BaseModel):
    id: str


class WebhookEvent(BaseModel):
    type: str
    data: EventData


@app.post("/api/send-welcome", status_code=202)
async def send_welcome(req: SendRequest):
    result = await bird.emails.send(
        from_="Bird <onboarding@bird.dev>",
        to=req.to,
        subject="Welcome",
        html="<p>It works.</p>",
    )
    if result.error:
        raise HTTPException(status_code=500, detail=result.error.message)
    return {"id": result.data.id}


@app.post("/webhooks/bird", status_code=200)
async def bird_webhook(request: Request):
    raw = await request.body()
    header = request.headers.get("bird-signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)

    if "t" not in parts or "v1" not in parts:
        raise HTTPException(status_code=401, detail="invalid_signature")
    if abs(time.time() - int(parts["t"])) > 300:
        raise HTTPException(status_code=401, detail="invalid_signature")

    secret = os.environ["BIRD_WEBHOOK_SECRET"].encode()
    message = f"{parts['t']}.".encode() + raw
    expected = hmac.new(secret, message, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(parts["v1"], expected):
        raise HTTPException(status_code=401, detail="invalid_signature")

    event = WebhookEvent.model_validate_json(raw)
    # event.type == "email.delivered" | "email.bounced" | ...
    return {"received": True, "id": event.data.id}

Run it:

fastapi dev main.py
curl -X POST http://localhost:8000/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 builds the same t.payload string the SDKs do, runs HMAC-SHA256 against the shared secret, and uses hmac.compare_digest to compare in constant time.

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.