uv add "beaco @ git+https://github.com/MichaelOgunjimi/notification-system.git@main#subdirectory=sdks/python"Python SDK
The official Python SDK publishes and queries Beaco events without adding a runtime dependency. It supports Python 3.9 and newer.
The SDK uses a secret project API key. Use it only from trusted server code and keep
BEACO_API_KEYout of source control.
Installation
The package is published on PyPI as beaco.
uv add beaco # or: pip install beacoPublish an Event
import os
from beaco import Beaco
beaco = Beaco(os.environ["BEACO_API_KEY"])
event = beaco.events.publish(
"user.welcome",
[{"channels": ["email"], "email": "[email protected]"}],
payload={"name": "Alice"},
idempotency_key="welcome-user-123",
)Query Events
page = beaco.events.list(status="completed", page=1)
event = beaco.events.retrieve("evt_123")Publish a batch atomically with beaco.events.publish_batch(events).
Available Resources
Events
beaco.events.publish(
event_type,
recipients,
inline={"subject": "Welcome", "html": "<h1>Welcome</h1>"},
)
beaco.events.publish_batch(events)
beaco.events.retrieve(event_id)
beaco.events.list(status="completed", page=1)Scopes: events:write for publishing and events:read for retrieving and listing.
Templates
template = beaco.templates.create(
"order-shipped",
"email",
"Hi {{ name }}, order {{ order_id }} is on its way.",
subject="Your order has shipped",
variables=["name", "order_id"],
)
beaco.templates.retrieve(template["id"])
beaco.templates.upsert_by_name(
"order-shipped",
"<p>Hi {{ name }}</p>",
text_body="Hi {{ name }}",
)
beaco.templates.import_html(
"welcome",
"<h1>Hi Alice</h1>",
{"name": "Alice"},
)
beaco.templates.list(channel="email")
beaco.templates.update(template["id"], subject="Updated subject")
beaco.templates.preview(template["id"], {"name": "Alice", "order_id": "ord_123"})
beaco.templates.delete(template["id"])Exactly one of template_id, template_name, or inline is required for immediate events.
Choose the sender with from_local (the part before the @), from_name, and reply_to on
inline, templates.create(), templates.upsert_by_name(), and templates.update(). The domain
always stays the server's verified domain; reply_to can be any valid address. Pass None to
update() to clear a field.
beaco.events.publish(
"order.confirmed",
[{"channels": ["email"], "email": "[email protected]"}],
inline={
"subject": "Order BEA-1042 confirmed",
"html": "<h1>Thanks for your order</h1>",
"from_local": "orders",
"from_name": "Winwell Orders",
"reply_to": "[email protected]",
},
)Attach files with attachments=[{"filename": ..., "url": ..., "size_bytes": ...}] on
events.publish(): up to 10, 30 MB declared in total. Beaco never stores them: the email provider
downloads each url when sending, so it must stay reachable until delivery succeeds, including
retries. Invalid attachments raise ValueError before any request is made.
beaco.events.publish(
"order.confirmed",
[{"channels": ["email"], "email": "[email protected]"}],
inline={"subject": "Your invoice", "html": "<p>Attached.</p>"},
attachments=[
{"filename": "invoice.pdf", "url": "https://files.example.com/a.pdf", "size_bytes": 48213}
],
)Scopes: templates:write for mutations and templates:read for retrieval, listing, and previews.
Notifications
notification = beaco.notifications.retrieve("ntf_123")
page = beaco.notifications.list(status="failed", channel="email")Scope: notifications:read.
Scheduled Events
scheduled = beaco.scheduled_events.create(
"renewal.reminder",
[{"channels": ["email"], "email": "[email protected]"}],
"2026-10-02T09:00:00Z",
template_name="renewal-reminder", # or template_id / inline, same as events.publish
payload={"renewal_date": "2026-10-03"},
)
beaco.scheduled_events.list(status="pending")
beaco.scheduled_events.cancel(scheduled["id"])Scheduled events accept the same content fields as events.publish (exactly one of template_id,
template_name or inline, plus optional attachments; no idempotency_key). Once the time
arrives the event is dispatched and its event_id is set; an event more than an hour overdue is
expired instead of sent. Cancelling is only possible while status is pending.
Scopes: scheduled_events:write for creation and cancellation, and scheduled_events:read for listing.
Suppressions
suppression = beaco.suppressions.create(
"email",
"[email protected]",
reason="manual",
)
beaco.suppressions.list(channel="email")
beaco.suppressions.delete(suppression["id"])Scopes: suppressions:write for creation and deletion, and suppressions:read for listing.
Local Development
Loopback HTTP is allowed for local development:
beaco = Beaco(
os.environ["BEACO_API_KEY"],
base_url="http://localhost:8000/api/v1",
timeout=15,
)Remote base URLs must use HTTPS unless allow_insecure_http=True is explicitly set.
Errors
Failed requests raise BeacoError with code, status, details, and retryable fields:
from beaco import BeacoError
try:
beaco.events.retrieve("evt_123")
except BeacoError as error:
if error.retryable:
print("retry later")All resource methods return decoded API dictionaries. List methods return the standard items, total, page, per_page, and total_pages fields.

