Skip to main content

sendandretain

The official Python SDK for the Send & Retain email API — send email, sync contacts, emit events that drive automations, and verify webhooks. Sync and asyncio.

pip install sendandretain    # or: uv add sendandretain

Python 3.11 or newer.

Quickstart

from sendandretain import SendAndRetain

client = SendAndRetain()  # reads SENDANDRETAIN_API_KEY

email = client.emails.send(
    to="jane@acme.com",
    template="welcome",
    props={"firstName": "Jane"},
)
print(email.id)  # queued — delivery is reported by webhook

A send is accepted with 202 and delivered in the background. Follow it with client.emails.get(email.id) or, better, a webhook.

Authentication

Mint a key in the dashboard. Every Send & Retain API key starts with aem_ and is sent as Authorization: Bearer aem_….

client = SendAndRetain("aem_...")  # or set SENDANDRETAIN_API_KEY and pass nothing

Scopes and the send grant

Authorization has two independent axes.

The scope is ranked — a key satisfies any requirement at or below its own tier:

Scope What it can do
read See messages, contacts, templates and metrics. Never changes anything, and cannot send.
write Everything read does, plus managing contacts, templates, automations, segments and suppressions.
admin Everything write does, plus domains, senders and workspace settings.

The send grant is a separate boolean, not a rung. Delivering mail to a real inbox — send_email, send_email_batch, reschedule_email and send_template_test — needs write and the grant; a key can hold admin and still be refused there. The dashboard mints that combination as Send + manage; Manage only is the same rung with the grant withheld.

A key that falls short gets 403, and the body says which axis refused it: required_scope / key_scope for the tier, or required_grant when the tier was enough but the key lacks the grant — not a generic denial.

Errors

Methods return the parsed model and raise on any failure. Every exception is a SendAndRetainError carrying code, status, request_id and the full error body:

from sendandretain import NotFoundError, QuotaExceededError, SendAndRetainError

try:
    client.templates.get("welcome")
except NotFoundError:
    ...
except QuotaExceededError:  # 429 daily_cap / monthly_cap — a quota, not a throttle
    ...
except SendAndRetainError as e:
    print(e.code, e.request_id)  # quote request_id to support
Exception When
InvalidRequestError 400 / 422 — fix the request; nothing changed
AuthenticationError 401 — missing, invalid or revoked key
PermissionDeniedError 403 — see e.body["required_scope"] / ["required_grant"]
NotFoundError 404
ConflictError 409 — an idempotency conflict, or the resource's state forbids it
RateLimitError 429 rate_limited, after the SDK's retries
QuotaExceededError 429 daily_cap / monthly_cap — never retried
APIError anything else, 5xx included
APIConnectionError no response arrived

Idempotency

Pass idempotency_key on any create to make it safe to retry:

client.emails.send(to="jane@acme.com", template="welcome", idempotency_key="welcome/user_42")

The same key and body replays the first result instead of sending again; a different body with the same key raises ConflictError. Keys live 24 hours. When you pass none, the SDK generates one per call, so its own retries can never send twice.

Retries and timeouts

408, 429 and 5xx responses, and requests that get no response, are retried twice with exponential backoff and jitter, honouring Retry-After — but only when safe: reads, updates, deletes, and creates carrying an idempotency key (all of them, by default). A sending quota is never retried.

client = SendAndRetain(max_retries=4, timeout=10.0)

Pagination

Lists return a page — .data, .has_more, .next_cursor. iterate walks every page:

for email in client.emails.iterate(status="bounced"):
    print(email.to)

page = client.contacts.list(limit=100)
if page.has_more:
    page = client.contacts.list(limit=100, cursor=page.next_cursor)

Under a resource: client.contacts.iterate_events(contact_id), client.automations.iterate_runs(automation_id), client.webhooks.deliveries.iterate(endpoint_id).

Async

AsyncSendAndRetain has the same resources, every method awaitable:

from sendandretain import AsyncSendAndRetain

async with AsyncSendAndRetain() as client:
    email = await client.emails.send(to="jane@acme.com", template="welcome")
    async for contact in client.contacts.iterate():
        ...

Webhooks

Subscribe an endpoint and store the secret it returns — it is shown once:

endpoint = client.webhooks.create(
    url="https://acme.com/webhooks/sendandretain",
    event_types=["email.delivered", "email.bounced", "email.complained"],
)

Verify every delivery against the raw body, then act on event["type"]:

from sendandretain import WebhookVerificationError, verify_webhook

# Flask: request.get_data() · FastAPI: await request.body() · Django: request.body
try:
    event = verify_webhook(raw_body, request.headers, secret=os.environ["SENDANDRETAIN_WEBHOOK_SECRET"])
except WebhookVerificationError:
    return "", 400

if event["type"] == "email.bounced":
    ...

Deliveries are Standard Webhooks, at-least-once and unordered: dedupe on event["id"], order on event["occurred_at"]. Verification is standard library only.

Resources

Resource Methods
emails send, get, list, iterate, reschedule, cancel, batch.send
contacts upsert, get, list, iterate, delete, import_, events, iterate_events
events emit
suppressions list, iterate, add, remove, import_
templates create, list, get, update, update_meta, archive, list_versions, publish, unpublish, render, test, add_translation
automations create, list, get, update, archive, set_status, duplicate, backfill, preflight, runs, iterate_runs, metrics, set_ab_test, promote_ab_winner, steps.{add,update,remove,move,sync_props}
segments create, list, update, delete, refresh_count
domains create, list, verify, delete
senders create, list, update, delete
webhooks create, list, get, update, delete, rotate_secret, test, deliveries.list, deliveries.iterate, deliveries.replay
metrics get, trends, queue_health
settings get, update, get_brand, update_brand
setup onboarding, connection, register_provider_webhook

Every API operation has a method; a test fails if one is missing. Body fields are keyword arguments with their wire names, except from, which is from_.

How it is organised

Path What it is
SendAndRetain, AsyncSendAndRetain _client.py, hand-written. Key, base URL, transport.
resource methods _resources.py, generated from openapi.json by scripts/generate_facade.py.
sendandretain.api.*, sendandretain.models Generated by openapi-python-client 0.29.0: one module per operation, attrs models. Use them directly for anything the facade does not shape your way; client.raw is the AuthenticatedClient they take.
_runtime.py The transport — retries, idempotency, User-Agent. Synced from the Send & Retain source tree.
_exceptions.py, _webhooks.py, _base.py Hand-written.
openapi.json The vendored spec everything is generated from.

sendandretain.Client — the 0.1 factory returning the generated AuthenticatedClient — still works.

Regenerating

The generated tree and the facade are committed. Regenerate both after changing openapi.json:

uv run python scripts/generate.py

And check both are in sync — this is what CI runs:

uv run python scripts/verify_schema_sync.py

scripts/normalize_spec.py runs first and rewrites two constructs the generator cannot model, without changing the contract: anonymous object/array members of an anyOf/oneOf are lifted into named schemas and replaced with a $ref, and a type list whose array branch carries no items is widened to an unconstrained schema. Both are deterministic, which is what makes the drift check meaningful.

Development

uv sync
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run python scripts/verify_schema_sync.py
uv run pytest

License

MIT — see LICENSE.

Metadata

Release files for sendandretain 0.2.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 sendandretain 0.2.0
File Size Uploaded
sendandretain-0.2.0.tar.gz 220.3 kB Details

Built distribution (wheel)

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

Total release size: 727.6 kB

Release files / sendandretain-0.2.0.tar.gz

Download URL sendandretain-0.2.0.tar.gz
Size 220.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f5bb2cc9187af1d7c3e4c87f318bdd1a44b728c0e4bbdba2de3d138a780be5ee
BLAKE2b-256 checksum
How to use checksums
f3963b68a2c041942f1ddc01f5778dd0a0707ada5a4e779794ab5708d8d48ea5
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 Oct 5, 2026.

Transparency log

Release files / sendandretain-0.2.0-py3-none-any.whl

Download URL sendandretain-0.2.0-py3-none-any.whl
Size 507.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08266d50790d9ae5b08f575934e9ede1ce49389930920c142ca65bf4f23005f5
BLAKE2b-256 checksum
How to use checksums
ea6361a647babc8641e768943036005b75e24e7421caab538813bd4971ba1561
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 Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

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