Bird Python SDK
The official Python SDK for the Bird API: email, SMS, WhatsApp, verification, and Realtime, over one typed client.
📚 Documentation: https://bird.com/docs/sdks/python
Status: in development. The PyPI distribution name is
messagebird-sdk; the import package isbird.
Requires Python 3.10+.
Install
pip install messagebird-sdk # or: uv add messagebird-sdk
This SDK is generated from Bird's public OpenAPI bundle inside Bird's internal monorepo, which is the single source of truth; this repository tracks tagged releases. Generation runs in the monorepo, so
make generatewon't work from a clone here — see CONTRIBUTING.md.
Quickstart
from bird import APIError, Bird
with Bird() as client:
try:
message = 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(message.id, message.status)
except APIError as err:
print("send failed:", err)
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:) to close the underlying HTTP connection pool.
# Send
message = client.email.send(from_="hi@acme.com", to=["c@x.com"], subject="Hi", text="hello")
# Fetch
message = client.email.get("em_01krd…")
# List — iterating the page auto-paginates across cursors
for message in client.email.list(status="delivered"):
print(message.id, message.status)
Client-wide email defaults
Defaults fill any unset send field; a per-send value always wins.
client = Bird(
api_key="bk_eu1_...",
email_defaults={"from_": "noreply@acme.com", "reply_to": ["support@acme.com"]},
)
client.email.send(to=["c@x.com"], subject="Receipt", text="…") # uses noreply@acme.com
A send carries exactly one kind of content: a template, or free-form text, image, video, audio, sticker, document or location. Free-form content is deliverable only inside an open 24-hour customer service window, and every send but a Bird-managed template needs from_.
# Send a template
message = client.whatsapp.send(
to="+31612345678",
template="bird_otp",
language="en",
components=[{"type": "body", "parameters": [{"type": "text", "text": "123456"}]}],
)
# Send free-form text
message = client.whatsapp.send(
to="+31612345678",
from_="+31687654321",
text={"body": "Your order has shipped!"},
)
# Fetch
message = client.whatsapp.get("wam_01krd…")
# List — iterating the page auto-paginates across cursors
for message in client.whatsapp.list(status=["delivered"]):
print(message.id, message.status)
Lookup
Every answer is billed. A phone number lookup bills once for the base answer plus once per delivered property; an email lookup bills once per answered address. Nothing is billed for a failed lookup or an unanswered property, so read status before you read a value.
# What is this number? The base answer is always present and always billed.
number = client.lookup.phone_number(phone_number="+31612345678", type=["porting", "score"])
print(number.country_code, number.line_type)
# Only a block whose status is "ok" carries a value, and only that one is billed.
if number.score is not None and number.score.status == "ok":
print(number.score.value)
# Is this address worth sending to? `result` is an open vocabulary, so fall back
# on delivery_confidence (0-100, always present) for a verdict you don't know.
address = client.lookup.email(email="aisha.khan@example.com")
print(address.result, address.delivery_confidence)
Pass an idempotency key so a retry replays the stored answer instead of buying a second one.
Realtime
Every Realtime call is scoped to one Realtime app and authenticated with that app's own key and secret — separate from your Bird API key — configured once on the client. Calling a Realtime method without them raises BirdError before any request is sent.
client = Bird(api_key="bk_eu1_...", realtime_key="rk_...", realtime_secret="rs_...")
# Publish one event to up to 100 channels
client.realtime.publish(
"rap_01krd…",
event="order-updated",
channels=["orders", "orders-42"],
data={"id": 42, "status": "shipped"},
exclude_connection_id="81721.1907241", # don't echo back to the client that acted
)
# Publish up to 10 events at once — each targets a single channel
client.realtime.publish_batch(
"rap_01krd…",
events=[
{"event": "order-created", "channel": "orders", "data": {"id": 1}},
{"event": "order-updated", "channel": "orders", "data": {"id": 2}},
],
)
# Live channel state — a snapshot, not a paginated collection
for channel in client.realtime.channels.list("rap_01krd…", prefix="presence-").data:
print(channel.name)
channel = client.realtime.channels.get("rap_01krd…", "presence-lobby", include=["member_count"])
members = client.realtime.channels.members("rap_01krd…", "presence-lobby")
# Close every connection authenticated as this member
client.realtime.members.disconnect("rap_01krd…", "member:42")
Webhooks
from bird import Bird, WebhookVerificationError
client = Bird(api_key="bk_eu1_...", webhook_secret="whsec_...")
# In your web handler — pass the RAW request body (bytes) and the request headers
try:
event = client.webhooks.unwrap(request.body, request.headers)
except WebhookVerificationError:
return Response(status=400)
if event.root.type == "email.delivered":
print("delivered:", event.root.data.message_id)
Endpoint management (registering/listing webhook endpoints) is not in this release; it returns once the delivery substrate stabilises.
Errors
Every failure raises a typed exception rooted at BirdError. APIError covers anything that goes wrong issuing a request — including transport failures — so a single except APIError is enough; APIStatusError carries the HTTP status_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)
Transient failures (timeouts, 429, 5xx) retry automatically with jittered backoff that honors Retry-After; a mutation reuses one idempotency key across attempts, so a retried write never double-applies.
Raw response
Reach the status, headers, and request_id alongside the parsed model:
raw = client.email.with_raw_response.send(from_="hi@acme.com", to=["c@x.com"], subject="Hi", text="…")
print(raw.status_code, raw.request_id)
message = raw.parse()
Async
AsyncBird mirrors Bird method-for-method — await each call and async for over a list:
import asyncio
from bird import AsyncBird
async def main() -> None:
async with AsyncBird(api_key="bk_eu1_...") as client:
await client.email.send(from_="hi@acme.com", to=["c@x.com"], subject="Hi", text="hello")
async for message in client.email.list(status="delivered"):
print(message.id)
asyncio.run(main())
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 via options. |
webhook_secret |
Signing secret for webhooks.unwrap. |
realtime_key / realtime_secret |
Realtime app credentials, sent as X-Realtime-Key / X-Realtime-Secret on every client.realtime call. |
email_defaults |
Client-wide send defaults. |
http_client |
Inject your own httpx.Client / AsyncClient. |
client.with_options(...) derives a new client (reusing the connection pool); every method also takes a trailing options for per-call timeout / max_retries / idempotency_key / extra_headers.
Escape hatch
Any endpoint outside the typed surface is reachable through the verb methods, with the same auth, retries, and idempotency:
from bird import EmailMessage
message = client.get("/v1/email/messages/em_01krd...", cast_to=EmailMessage)
client.post("/v1/some/new/endpoint", body={"key": "value"})
Design
The wire models are generated from the OpenAPI spec into bird._generated; this package is the hand-written idiomatic layer on top.
Release files for messagebird-sdk 0.45.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| messagebird_sdk-0.45.0.tar.gz | 160.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| messagebird_sdk-0.45.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 354.2 kB
Release files / messagebird_sdk-0.45.0.tar.gz
| Download URL | messagebird_sdk-0.45.0.tar.gz |
|---|---|
| Size | 160.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
96328df58cb4a3be66f4107471eaf0ec0fbccd59195f5db06fe1da87c6709c13
|
|
BLAKE2b-256 checksum How to use checksums |
3024a7f13258270ad8f5dc1483cfde5ff528017b02a30e73f685a5d2c7c04f4a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency logRelease files / messagebird_sdk-0.45.0-py3-none-any.whl
| Download URL | messagebird_sdk-0.45.0-py3-none-any.whl |
|---|---|
| Size | 193.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9ddf37b8586dc9dfbb82fe6f27c20d148d8e277802d34ff4d91292466a2ed536
|
|
BLAKE2b-256 checksum How to use checksums |
0dc2a76b5aba91e1b97bcdaab4d4d01cec5d163f77cf86c468a35308f6ee3a18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency log