Documentation
Sign inGet started

Python SDK

messagebird-sdk (import name bird) is the official Python SDK for the Bird API. This page covers installation, configuration, errors, retries, pagination, and webhooks. To send email with the SDK, start with the Python email quickstart.

Install

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

Create a client

Choose between two clients: Bird (sync) and AsyncBird (async). They provide the same methods. With AsyncBird, use await for each call and async for over lists. Configuration uses keyword arguments:
Code example
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 and 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. Construct one client and reuse it; both clients 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:
Code example
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 specification. A hand-written layer provides the curated resource surface (client.email, client.webhooks), explicit keyword arguments, and a request lifecycle shared by every method. See SDK concepts for the cross-SDK model.

Errors

Failures raise typed exceptions rooted at BirdError. APIError covers request failures, including transport failures such as timeouts, so a single except APIError handles any failed call. APIStatusError is the server-returned subset, carrying status_code, request_id, code (the stable E##### code), and type (the coarse error category). Its subclasses include RateLimitError (a 429, with retry_after in seconds) and ValidationError (a 422, with per-field details):
Code example
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 are subclasses of APIError, so a broad except APIError catches them. A bad webhook signature raises WebhookVerificationError.

Safe retries

Transient failures, including timeouts, 429 responses, and 5xx responses, retry automatically with jittered backoff that honors Retry-After. Tune the budget with max_retries, or use zero to disable retries. A mutation generates one idempotency key per logical call and reuses it across every attempt. Pass idempotency_key in the per-call options to set your own.

Pagination

List methods return a lazy page (SyncPage / AsyncPage); iterating it auto-paginates across cursors, fetching pages on demand:
Code example
for message in client.email.list(status="delivered"):
    print(message.id)
Code example
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 them breaks the signature:
Code example
# 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 performs 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:
Code example
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