Skip to main content

bavimail-python

Python SDK for the Bavimail email service API.

Version 1.0 targets the current Bavimail API. Upgrading from 0.x? Read MIGRATION.md.

Installation

pip install bavimail

Quick Start

Synchronous

from bavimail import Bavimail

client = Bavimail(api_key="YOUR_API_KEY")

# Send an email from an inbox on a verified domain
email = client.emails.send(
    inbox_id="your-inbox-id",
    to_emails=["recipient@example.com"],
    subject="Hello!",
    body="<p>Welcome!</p>",
)
print(f"Accepted: {email.id} (status: {email.status})")

client.close()

The API key travels in the x-api-key header. Sends are accepted as queued (or scheduled) and delivered by Bavimail; follow them with client.emails.get(email.id) or webhooks.

Asynchronous

Every method has an _async variant. Use async with for automatic cleanup:

import asyncio
from bavimail import Bavimail

async def main():
    async with Bavimail(api_key="YOUR_API_KEY") as client:
        domains = await client.domains.list_async()
        email = await client.emails.send_async(
            inbox_id="your-inbox-id",
            to_emails=["recipient@example.com"],
            subject="Hello!",
            body="<p>Welcome!</p>",
        )

asyncio.run(main())

Resources

Domains

# 1. Publish the account's ownership TXT record (it must stay published)
ownership = client.domains.get_ownership("example.com")
print(f"{ownership.record.type} {ownership.record.name} -> {ownership.record.value}")

# 2. Add the domain and publish its setup records
domain = client.domains.create("example.com")
setup = client.domains.get_setup(domain.id)
for record in setup.dns_records:
    print(f"{record.type} {record.name} -> {record.value}")

# Or let Bavimail publish them through your DNS provider's API
options = client.domains.get_setup_options(domain.id)
if options.detected_provider.supports_api_setup:
    client.domains.auto_setup(domain.id, options.detected_provider.id, {"api_token": "..."})

# 3. Bavimail verifies the domain on its own; watch the progress
status = client.domains.get_setup_status(domain.id)
print(f"Verified checks: {status.progress.verified}/{status.progress.total}, ready={status.ready}")

domain = client.domains.update(domain.id, inbound_enabled=False)
client.domains.delete(domain.id)

Inboxes

inbox = client.inboxes.create(domain.id, "support")  # support@example.com
inboxes = client.inboxes.list(domain_id=domain.id)
client.inboxes.update(inbox.id, signature_html="<p>The team</p>")
client.inboxes.delete(inbox.id)

Emails (Outbound)

email = client.emails.send(
    inbox_id=inbox.id,
    to_emails=["user@example.com"],
    subject="Welcome",
    body="<h1>Hello!</h1>",
    track_opens=True,
    headers={"X-Campaign": "welcome"},
    attachments=[
        {"filename": "doc.pdf", "content": "<base64>", "content_type": "application/pdf"},
        {"attachment_id": "uploaded-attachment-id"},
    ],
)

# Schedule (local time in an IANA timezone) and cancel
later = client.emails.send(
    inbox.id, ["user@example.com"], "Tomorrow", "<p>Hi</p>",
    scheduled_at="2026-10-20T09:00:00", scheduled_timezone="Europe/Rome",
)
client.emails.cancel(later.id)

page = client.emails.list(inbox_id=inbox.id, status="delivered", limit=50)
print(page.total, [e.subject for e in page.data])

events = client.emails.list_events(email.id)
tracking = client.emails.get_tracking(email.id)
result = client.emails.batch_send([
    {"inbox_id": inbox.id, "to_emails": ["a@example.com"], "subject": "Hi", "body": "Hello"},
])
check = client.emails.validate("someone@example.com")

Inbound Emails and Threads

page = client.inbound_emails.list(inbox_id=inbox.id, limit=25)
detail = client.inbound_emails.get(page.data[0].id)
data = client.inbound_emails.download_attachment(detail.id, detail.attachments[0].id)

threads = client.threads.list(unread=True)
thread = client.threads.get(threads.data[0].id)
for message in thread.messages:
    print(message.type, message.subject)

client.threads.update(thread.id, unread=False, add_label_ids=["label-id"])
client.threads.update(thread.id, folder_id="trash")
client.threads.delete(thread.id)  # permanent, only from trash

Labels and Mailing Lists

label = client.labels.create("Important", color="#ff0000")
folder = client.labels.create("Projects", kind="folder")
labels = client.labels.list()

lists = client.mailing_lists.list(status="active")
client.mailing_lists.unsubscribe(lists.data[0].id, trash_threads=True)

Attachments

from bavimail import AttachmentUploadFile

files = client.attachments.upload([AttachmentUploadFile("doc.pdf", b"...", "application/pdf")])
content = client.attachments.download(files[0].id)
client.attachments.delete(files[0].id)

Downloads use the signed URL Bavimail returns; the API key is never sent to storage.

Webhooks

# Create (returns the secret, shown once!)
wh = client.webhooks.create(
    url="https://hooks.example.com/bavimail",
    event_types=["email.inbound.received", "domain.verified"],
)
print(f"Secret: {wh.secret}")  # Store this!

# Bavimail posts a signed {"type": "webhook.verification", "verification_code": ...}
# challenge to the URL; activate the webhook with that code
wh = client.webhooks.verify(wh.id, "verification-code-from-endpoint")

webhooks = client.webhooks.list()
client.webhooks.update(wh.id, status="disabled")
client.webhooks.test(wh.id)
secret = client.webhooks.rotate_secret(wh.id)
client.webhooks.delete(wh.id)

Suppressions, Warmup, Tracking Domains, Reputation

client.suppressions.create("blocked@example.com", note="asked to stop")
page = client.suppressions.list(reason="bounce")

warmup = client.warmup.enable(inbox.id, "bavimail", daily_volume=20)
client.warmup.update(warmup.id, daily_volume=40)
client.warmup.pause(warmup.id)

tracking = client.tracking_domains.create("track.example.com")
client.tracking_domains.verify(tracking.id)

reputation = client.reputation.get()

Agent self-registration

from bavimail import Bavimail

result = Bavimail.register(name="my-agent")  # no credentials needed
client = Bavimail(api_key=result.api_key.key)
print("Ask a person to claim the account:", result.claim.claim_url)

Pagination

Offset lists return a Page (data, total); threads and the event feed return a CursorPage (data, next_cursor). Iterate everything with the helpers:

from bavimail import iter_cursor_pages, iter_pages, iter_pages_async

for email in iter_pages(client.emails.list, inbox_id="...", page_size=50):
    print(email.subject)

for thread in iter_cursor_pages(client.threads.list, unread=True):
    print(thread.subject)

async for email in iter_pages_async(client.inbound_emails.list_async, page_size=25):
    print(email.subject)

Receiving Webhooks

from bavimail import Bavimail, EventType, WebhookEvent
from bavimail.events import InboundReceivedData

client = Bavimail(api_key="YOUR_API_KEY")

@client.on(EventType.INBOUND_RECEIVED)
def handle(event: WebhookEvent):
    data = InboundReceivedData.from_dict(event.data)
    print(data.from_email, data.subject)

# Standalone server: creates and verifies the webhook, deletes it on exit
client.listen(port=8080, webhook_url="https://my-server.example.com/webhooks")

# Or mount into Flask / FastAPI with an existing secret
blueprint = client.webhook_blueprint("/webhooks", secret="your-hex-secret")
router = client.webhook_router(
    "/webhooks",
    secret="your-hex-secret",
    on_verification=lambda code: client.webhooks.verify("webhook-id", code),
)

Verification challenges and test deliveries are answered with 200 and are not dispatched to event handlers.

Signature verification

from bavimail import verify_webhook_signature, WebhookVerificationError

try:
    verify_webhook_signature(
        payload=request.body,
        signature=request.headers["x-webhook-signature"],
        timestamp=request.headers["x-webhook-timestamp"],
        secret="your-hex-secret",
    )
except WebhookVerificationError as e:
    print(f"Invalid webhook: {e}")

Error Handling

All API errors raise typed exceptions carrying the API's machine-readable code:

from bavimail import (
    APIError,
    NotFoundError,
    QuotaExceededError,
    ValidationError,
)

try:
    client.emails.send(inbox_id, ["user@example.com"], "Hi", "Hello")
except ValidationError as e:
    print(e.code, e.message)       # e.g. "recipient_suppressed"
except QuotaExceededError as e:
    print("Plan limit reached:", e.code)
except APIError as e:
    print(f"API error ({e.status_code}): {e.message}")
Status Exception
400 ValidationError
401 AuthenticationError
402 QuotaExceededError
403 ForbiddenError
404 NotFoundError
409 ConflictError
429 RateLimitError
500 InternalServerError
502/503 ProviderError

Custom HTTP Client

Pass a custom httpx.Client for advanced configuration (proxies, custom TLS, retries):

import httpx

custom = httpx.Client(
    base_url="https://api.bavimail.com",
    headers={"x-api-key": "YOUR_API_KEY"},
    transport=httpx.HTTPTransport(retries=3),
    timeout=60.0,
)

client = Bavimail(api_key="YOUR_API_KEY", http_client=custom)

Development

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Type check
mypy src/bavimail --strict

# Lint
ruff check src/ tests/

License

MIT

Metadata

Release files for bavimail 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bavimail 1.0.0
File Size Uploaded
bavimail-1.0.0.tar.gz 43.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bavimail 1.0.0
File Interpreter ABI Platform
bavimail-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 109.5 kB

Release files / bavimail-1.0.0.tar.gz

Download URL bavimail-1.0.0.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
997f3c7c6b44b33c1c924a6ce604ca6189f996567ce152854fa0b6ec2ad76151
BLAKE2b-256 checksum
How to use checksums
ba0d83d7a6246fb543ec08b7860f5911df55aa8257d88eb065f230fe75caeef9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / bavimail-1.0.0-py3-none-any.whl

Download URL bavimail-1.0.0-py3-none-any.whl
Size 65.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1b0e1e130fcd0245cb684355b4a6e4b98b9c6f7940c8fb34caf119997b869317
BLAKE2b-256 checksum
How to use checksums
977cdc8910e578afd53766f9d2922c130c7ad32de1b0fc46a42edfb05045f036
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

1 release file

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page