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

Inizia con un canale.
Aggiungi gli altri quando sei pronto.

Una chiave API di test è subito tua. La produzione si sblocca quando aggiungi un metodo di pagamento e verifichi un mittente.

Usi Claude Code, Cursor o Codex? Puntali al nostro server MCP — 141 strumenti, uno per endpoint API, con chiavi agent a scope limitato.