Skip to main content

Sendara — email API (transactional + marketing email, broadcasts, contacts, templates, domains, webhooks) for Python.

Project description

Sendara — Python SDK

The official Python client for the Sendara email API: transactional and marketing email, broadcasts, contacts and lists, templates, domains, and signed webhook events — with sync and async clients, typed models, a typed error hierarchy, automatic retries, and an auto-paginating message iterator.

pip install sendara

Requires Python 3.9+. Runtime dependency: httpx.

Quickstart

import os
from sendara import Sendara, SendaraError

client = Sendara(os.environ["SENDARA_API_KEY"])  # sk_live_... or sk_test_...

# Send an email
result = client.emails.send(
    from_="hello@acme.com",
    to="user@example.com",
    subject="Welcome to Acme",
    html="<h1>Welcome 🎉</h1>",
)
print(result.id, result.status)

# Paginate every message (cursor handled for you)
for message in client.messages.iter(limit=100):
    print(message.id, message.status)

Async

import asyncio
from sendara import AsyncSendara

async def main():
    async with AsyncSendara("sk_live_...") as client:
        await client.emails.send(
            from_="hello@acme.com",
            to="user@example.com",
            subject="Hi",
            html="<p>Hi</p>",
        )
        async for message in client.messages.iter(limit=100):
            print(message.id)

asyncio.run(main())

Both clients are context managers (with / async with) and accept base_url, timeout, max_retries, and an optional http_client.

client = Sendara(
    "sk_live_...",
    base_url="https://api.sendara.dev",
    timeout=30.0,
    max_retries=3,
)

Idempotency

Every send accepts an idempotency_key. The SDK generates one (UUIDv4) automatically when you omit it and reuses that value for retries within the same call. Pass your own to deduplicate across process restarts or separate calls:

client.emails.send(
    from_="hello@acme.com",
    to="u@e.com",
    subject="s",
    text="t",
    idempotency_key="order-42",
)

Retries

Idempotent requests (all GET/PUT/DELETE and sends) are retried automatically on 429, 409, 5xx, and network/timeout errors, using exponential backoff with jitter. A Retry-After header is honored when present. Attempts are bounded by max_retries.

Errors

Every failure raises a subclass of SendaraError carrying status, code, message, and request_id:

HTTP Exception
400 / 422 ValidationError
401 AuthenticationError
403 PermissionError_
404 NotFoundError
409 ConflictError
429 RateLimitError (.retry_after)
5xx ServerError
network APIConnectionError / APITimeoutError
from sendara import RateLimitError, SendaraError

try:
    client.emails.send(to="u@e.com", subject="s", html="<p>x</p>")
except RateLimitError as e:
    print("retry after", e.retry_after)
except SendaraError as e:
    print(e.status, e.code, e.message)

Verifying webhooks

webhooks.verify recomputes HMAC-SHA256(secret, "<timestamp>.<raw_body>") (hex) and checks it in constant time against the Sendara-Signature header. Pass the raw request body, never a re-serialized object.

from sendara import webhooks

# Flask example
@app.post("/webhooks/sendara")
def hook():
    event = webhooks.verify(
        os.environ["SENDARA_WEBHOOK_SECRET"],
        request.get_data(),     # raw bytes
        request.headers,        # case-insensitive mapping
    )
    print(event["event_type"], event["message_id"])
    return "", 200

verify raises WebhookVerificationError on any mismatch (bad signature, missing headers, or a timestamp outside the tolerance window, default 300s).

Method reference

client.emails.send(to=, subject=, html=, text=, from_=, message_type=,
                   template_id=, template_vars=, idempotency_key=, metadata=,
                   store_payload=, test_send=, scheduled_at=, validate_recipient=)
client.emails.send_raw(request)            # POST /v1/send (escape hatch)
client.emails.send_batch([req, ...])       # POST /v1/send/batch
client.emails.send_bulk(request)           # POST /v1/send/bulk

client.broadcasts.create(**params) / .list(limit=, offset=) / .get(id)
                 .send(id) / .cancel(id) / .delete(id)
    # audience params: list_ids=[...], exclude_list_ids=[...], audience_list_id=,
    # or recipients=[...]. Same keys apply to emails.send_bulk(request).

client.messages.list(channel=, status=, search=, from_=, to=, limit=, cursor=)
client.messages.iter(channel=, status=, search=, from_=, to=, limit=)   # auto-paginating
client.messages.get(id) / .get(idempotency_key=)   # full event timeline

client.suppressions.list(channel=) / .create(channel=, recipient=, reason=)
                    .delete(channel=, recipient=)

client.domains.list() / .create(domain) / .get(domain) / .verify(domain)

client.api_keys.list() / .create(scope=, test_mode=) / .rotate(id) / .revoke(id)

client.usage.get(period=)
client.usage.set_spend_cap(key_id=, soft_limit_micros=, hard_limit_micros=)

client.billing.get() / .checkout(plan=, period=) / .portal()

client.templates.create(**params) / .list() / .get(id) / .update(id, **params)
                .delete(id) / .render(id, vars=)

client.contacts.create(**params) / .list(limit=, offset=) / .get(id)
               .update(id, **params) / .delete(id) / .import_(s3_key=, format=)
client.lists.create(name=, list_type=, segment_rules=) / .list() / .get(id)
            .update(id, **params) / .delete(id)
            .add_member(id, contact_id) / .remove_member(id, contact_id) / .members(id)

client.webhooks.create(endpoint_url=, event_types=) / .list() / .get(id)
               .update(id, **params) / .delete(id)
               .deliveries(id, limit=) / .rotate_secret(id)

client.uploads.create(file, filename=, content_type=)   # multipart image

client.test_recipients.list() / .create(email) / .resend(id) / .delete(id)

webhooks.verify(secret, payload, headers=, signature=, timestamp=, tolerance=300)

The async client (AsyncSendara) exposes every method above with await; messages.iter becomes an async for.

billing.checkout accepts starter, pro, growth, or scale, with a month or year period. It returns a CheckoutResult for both successful outcomes:

checkout = client.billing.checkout(plan="growth", period="year")
if checkout.updated:
    print("subscription changed to", checkout.plan)
else:
    redirect(checkout.url)  # hosted checkout for a new subscription

Development

cd sdk/python
pip install -e '.[dev]'
python -m pytest

MIT licensed.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sendara-0.5.0.tar.gz (25.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sendara-0.5.0-py3-none-any.whl (24.2 kB view details)

Uploaded Python 3

File details

Details for the file sendara-0.5.0.tar.gz.

File metadata

  • Download URL: sendara-0.5.0.tar.gz
  • Upload date:
  • Size: 25.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.13

File hashes

Hashes for sendara-0.5.0.tar.gz
Algorithm Hash digest
SHA256 c01f5b6529c1818834321eabb2921c4acfad0a0fca9813783bfacb1adbe7919a
MD5 6552d99b48f7b2aebccf98689c044a18
BLAKE2b-256 bb6e151ca2785a0e5c170de72a2708ed14bd467fe1e29231116f4275e65226af

See more details on using hashes here.

File details

Details for the file sendara-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: sendara-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 24.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.13

File hashes

Hashes for sendara-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c649985d6b2c47bf9dae99c8126a1156d4907edf40608f46b20df47e2e502111
MD5 7d48bbdd23737741de652159286cfbd7
BLAKE2b-256 d7d1223df33a4a783f06660d3e611e0fc9c0a86df69c1b03a1db292bd9bee05e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page