Skip to main content

outboxstack

Python client for Outbox Stack email. Python 3.9+, no dependencies.

pip install outboxstack

Create a Sending only API key under Developers in the dashboard and set it as OUTBOX_API_KEY. For tests and staging, create a Test key instead: it checks every message but delivers nothing.

Send

from outboxstack import Outbox

outbox = Outbox()  # reads OUTBOX_API_KEY; base URL defaults to https://outboxstack.app

result = outbox.send({
    "from": "billing@your-verified-domain.com",
    "fromName": "Your app",
    "to": "Ada Obi <ada@example.com>",   # or a list
    "replyTo": "support@your-verified-domain.com",
    "subject": "Your invoice",
    "html": "<p>Invoice attached.</p>",
    "attachments": [{"filename": "invoice.pdf", "content": pdf_bytes}],
    "metadata": {"invoice_id": "inv_123"},
}, idempotency_key=f"invoice:{invoice.id}")

result["id"]          # the first recipient's message id
result["recipients"]  # one entry per recipient when there's more than one

Fields use the API's names: to, cc, bcc (a string or a list, 50 recipients in total), fromName, toName, replyTo, subject, html, text, template with data, headers (20), metadata (10 strings) and attachments (10 files, 10 MB). Attachment content can be raw bytes; the client encodes it.

send() returns once the message is queued, not delivered. Each recipient becomes its own message with its own id, status and bounce tracking.

Every send carries an idempotency key, generated when you don't pass one, so an automatic retry never sends twice. Pass your own to stay safe across process restarts too.

Also: send_batch(messages) (1–100 messages, each may carry idempotencyKey) and get_message(id).

Find messages

recent = outbox.list_messages(status="bounced", limit=100)   # newest 100

for m in outbox.iter_messages(status="bounced", to="@example.com"):
    print(m["id"], m["to_email"], m["created_at"])          # every match, a page at a time

To page by hand, call list_messages_page() and pass its next back as after until it's None.

Cancel, suppress

from outboxstack import OutboxApiError

try:
    outbox.cancel_message(message_id)
except OutboxApiError as e:
    if e.code != "not_cancellable":   # it already started sending
        raise

outbox.add_suppression("ada@example.com")       # needs a full-access key
outbox.remove_suppression("ada@example.com")    # manual suppressions only
for s in outbox.iter_suppressions():
    print(s["email"], s["reason"])

Errors

Exception When
OutboxApiError The API refused the request. Branch on e.code (domain_not_verified, rate_limited, quota_exceeded, …); quote e.request_id to support.
OutboxConnectionError No answer after every retry (e.code is timeout or connection_error). The request may have arrived: retry with e.idempotency_key.
OutboxError Bad input caught before any request, such as an invalid message id.

Timeouts, connection errors, 408, 429 and 5xx are retried twice with backoff, honouring Retry-After (max_retries=, timeout= seconds per attempt).

Webhooks

from outboxstack import verify_webhook, WebhookVerificationError

# Flask example: pass the raw body, not re-serialised JSON.
@app.post("/hooks/outbox")
def outbox_hook():
    try:
        event = verify_webhook(request.get_data(), request.headers, os.environ["OUTBOX_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return "", 400
    if event["type"] == "message.bounced":            # a hard bounce: final
        mark_undeliverable(event["data"]["to"])
    # message.deferred is a soft bounce or delay: not final, so don't give up on the address.
    return "", 200

message.bounced and message.deferred carry bounce_type, bounce_subtype, diagnostic and soft_bounce_count, and suppressed says whether the event added the address to your suppressions. Events from test API keys have test: True. The WebhookEvent and MessageEventData types describe every field.

Events can arrive more than once: deduplicate on the webhook-id header. See the event reference.

Licence

MIT

Metadata

Release files for outboxstack 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 outboxstack 0.2.0
File Size Uploaded Provenance
outboxstack-0.2.0.tar.gz 13.8 kB With provenance Details

Built distribution (wheel)

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

Total release size: 25.1 kB

Release files / outboxstack-0.2.0.tar.gz

Download URL outboxstack-0.2.0.tar.gz
Size 13.8 kB
Tags Source
SHA-256 checksum
How to use checksums
03188a94aea0b8f39dac9ccf16fc9963a9c6b0227b39c4d267e683a61f5a75c8
BLAKE2b-256 checksum
How to use checksums
27e49d3e1b0c30803526a995d034fe7bbf033ca0ba42dc14e047e537b844ef34
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 21, 2026.

Transparency log

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

Download URL outboxstack-0.2.0-py3-none-any.whl
Size 11.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6d878e0eff1bb1d6d512156dcb1a831b8dcada0fdbf0eaf56a79665af4408282
BLAKE2b-256 checksum
How to use checksums
4b2cfd5c43e58f9e1854a77f82476d4653023581bf7209c83177f6fb85ad784a
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 21, 2026.

Transparency log

Security for outboxstack 0.2.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for outboxstack-0.2.0.tar.gz

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 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

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