Documentation
Sign inGet started

Python SDK

bird-sdk (import name bird) is the official Python SDK for the Bird API. This page covers the client itself — install, configuration, errors, retries, pagination, and webhooks. The per-method reference lives on the channel pages, starting with Email.

Install

Exemple de code
pip install bird-sdk
Exemple de code
# or
uv add bird-sdk
poetry add bird-sdk
Requires Python 3.10+. The SDK is fully typed (py.typed), with Pydantic v2 response models.

Create a client

There are two clients: Bird (sync) and AsyncBird (async). They mirror each other method-for-method — await each call, async for over lists. Configuration is plain keyword arguments:
Exemple de code
msg = client.email.send(
    from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
    to=["delivered@messagebird.dev"],
    subject="Hello from Bird",
    html="<p>My first Bird email.</p>",
)
print(msg.id, msg.status)
from_ is the Python spelling of the wire field from (from is a reserved word); the alias is handled for you. Responses are Pydantic v2 models that tolerate unknown fields, so a new server field never breaks an existing client.
api_key and base_url fall back to the BIRD_API_KEY / BIRD_BASE_URL environment variables, so Bird() with no arguments works when they are set. Use the client as a context manager (with Bird() as client: / async with AsyncBird() as client:) to close the underlying connection pool — and construct one client and reuse it; both are safe to share across threads or tasks.

Configuration

OptionDescription
api_keyAPI key; falls back to BIRD_API_KEY.
region / base_urlRegion (or explicit base URL); falls back to the key prefix / BIRD_BASE_URL.
timeout, max_retriesRequest timeout and retry budget; overridable per call.
webhook_secretSigning secret for client.webhooks.unwrap.
email_defaultsClient-wide send defaults; a per-send value always wins.
http_clientInject your own httpx.Client / httpx.AsyncClient.
Every method also takes a trailing options for per-call timeout / max_retries / idempotency_key / extra_headers, and client.with_options(...) derives a new client that reuses the parent's connection pool:
Exemple de code
client.email.send(
    from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
    to=["delivered@messagebird.dev"],
    subject="Hello from Bird",
    text="My first Bird email.",
    options={"timeout": 10, "max_retries": 0},
)

How it's built

The wire models are generated from Bird's OpenAPI spec; everything you touch is a hand-written, idiomatic layer on top — the curated resource surface (client.email, client.webhooks), explicit keyword arguments, and the request lifecycle (retries, timeouts, idempotency) in one core shared by every method. The cross-SDK model is in SDK concepts.

Errors

Failures raise typed exceptions rooted at BirdError. APIError covers anything that goes wrong issuing a request — including transport failures like timeouts — so a single except APIError is enough for "the call failed". APIStatusError is the server-returned subset, carrying status_code, request_id, code (the stable E##### code), and type (the coarse error category). Below it sit RateLimitError (a 429, with retry_after in seconds) and ValidationError (a 422, with per-field details):
Exemple de code
from bird import APIStatusError, RateLimitError, ValidationError

try:
    client.email.send(
        from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
        to=["delivered@messagebird.dev"],
        subject="Hello from Bird",
        text="My first Bird email.",
    )
except RateLimitError as err:
    print("rate limited; retry after", err.retry_after)
except ValidationError as err:
    print(err.status_code, err.details)
except APIStatusError as err:
    print(err.status_code, err.code, err.request_id)
Transport-only failures are APIConnectionError and APITimeoutError — both subclasses of APIError, so they never slip past a broad except APIError. A bad webhook signature raises WebhookVerificationError.

Safe retries

Transient failures — timeouts, 429s, 5xx — retry automatically with jittered backoff that honors Retry-After (tune the budget with max_retries; zero disables). A mutation generates one idempotency key per logical call and reuses it across every retry attempt, so a retried send never double-applies. Pass idempotency_key in the per-call options to pin your own.

Pagination

List methods return a lazy page (SyncPage / AsyncPage); iterating it auto-paginates across cursors, fetching pages on demand:
Exemple de code
for message in client.email.list(status="delivered"):
    print(message.id)
Exemple de code
from bird import AsyncBird

async with AsyncBird() as client:
    async for message in client.email.list(status="delivered"):
        print(message.id)
Stop iterating and no further pages are fetched.

Webhooks

client.webhooks.unwrap verifies a Standard Webhooks signature over the raw request body and returns a typed, discriminated event. Configure the signing secret on the client (webhook_secret=), and pass the exact bytes you received — parsing and re-serializing first breaks the signature:
Exemple de code
# Pass the RAW request body (bytes) and the request headers.
event = client.webhooks.unwrap(request.body, request.headers)
if event.root.type == "email.delivered":
    print(event.root.data.email_id)
Verification is pure crypto — no network call — so it works the same in any web framework.

Escape hatch

Endpoints not yet on the typed surface are reachable through client.get / post / put / patch / delete, with the same auth, retries, and idempotency handling:
Exemple de code
from bird import EmailMessage

message = client.get("/v1/email/messages/em_01krd...", cast_to=EmailMessage)
client.post("/v1/some/new/endpoint", body={"key": "value"})
Find the paths in the API reference.

Next steps