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 dashboard —
bird_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
- Verify a sending domain — graduate from test keys to production sends.
- Send an SMS — same auth, same response envelope.
- Webhooks deep-dive — the full event catalog and retry schedule.
- Drop the MCP server in your IDE — let Claude and Cursor send through Bird.