# Python SDK

`messagebird-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. For sending email with the SDK, start with the [Python email quickstart](/docs/get-started/quickstarts/python/email).

## Install

```bash
pip install messagebird-sdk
```

```bash
# 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

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:

<!-- bird:snippet email.send -->

```python
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

| Option                   | Description                                                                    |
| ------------------------ | ------------------------------------------------------------------------------ |
| `api_key`                | API key; falls back to `BIRD_API_KEY`.                                         |
| `region` / `base_url`    | Region (or explicit base URL); falls back to the key prefix / `BIRD_BASE_URL`. |
| `timeout`, `max_retries` | Request timeout and retry budget; overridable per call.                        |
| `webhook_secret`         | Signing secret for `client.webhooks.unwrap`.                                   |
| `email_defaults`         | Client-wide `send` defaults; a per-send value always wins.                     |
| `http_client`            | Inject 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:

<!-- bird:snippet email.options -->

```python
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](/docs/sdks/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](/docs/guides/errors)). Below it sit `RateLimitError` (a 429, with `retry_after` in seconds) and `ValidationError` (a 422, with per-field `details`):

<!-- bird:snippet email.errors -->

```python
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:

<!-- bird:snippet email.list.iterate -->

```python
for message in client.email.list(status="delivered"):
    print(message.id)
```

<!-- bird:snippet email.list.async -->

```python
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](https://www.standardwebhooks.com/) 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:

<!-- bird:snippet webhook.unwrap -->

```python
# 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:

<!-- bird:snippet client.verbs -->

```python
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](/docs/api).

## Next steps

- [Python email quickstart](/docs/get-started/quickstarts/python/email) — first send in three steps: `send`, `get`, `list`, and every param.
- [SDK concepts](/docs/sdks/concepts) — the cross-SDK model: errors, idempotency, pagination, webhooks.
- [API reference](/docs/api) — the underlying HTTP contract.