Skip to main content

sadasend

PyPI version Python versions License: MIT Zero Dependencies Tests Passing Typing: Typed

The official Python SDK for SadaSend — high-throughput transactional email with unbypassable server-enforced guardrails designed for human software engineering teams and autonomous AI agents.

Zero external dependencies. Built strictly on the Python standard library (urllib.request, hmac, hashlib, json). No transitive dependencies, no version pinning conflicts, no vulnerability CVE alerts, and zero supply-chain risk.


Why SadaSend?

Legacy email APIs (SendGrid, Mailgun, Postmark, AWS SES) were built for human backends. When you hand an API key to an autonomous LLM agent (LangChain, CrewAI, AutoGen, Claude Code, Cursor), a hallucination or prompt injection can blast spam or burn your sending domain reputation. SadaSend is built from day one with unbypassable server-enforced guardrails.

Feature SadaSend SendGrid Mailgun Postmark AWS SES
Runtime Dependencies 0 (Standard Library Only) 8+ dependencies requests / urllib3 requests boto3 (heavy)
Agent Recipient Allowlist Server-Enforced None (Manual) None None None
Human Approval Mode Native (pending_approval) None None None None
Built-in Idempotency Automatic (Guaranteed Replay) None Manual None None
Retry-After Backoff Automatic Full Jitter Manual Naive Manual Manual
Native MCP Server Native (POST /mcp) None None None None
Typed Exceptions with Fixes Yes (records, allowlist) Generic HTTP 400 Generic HTTP 400 Generic HTTP 422 Generic ClientError
Preflight Dry Runs Native (dry_run=True) None None None None

Installation

# pip
pip install sadasend

# poetry
poetry add sadasend

# uv
uv add sadasend

Works out of the box on Python 3.9, 3.10, 3.11, 3.12, and 3.13. Ideal for serverless environments (AWS Lambda, Google Cloud Functions, Cloudflare Python Workers) and Docker micro-containers due to zero external package installation footprint.


10-Line Quickstart

import os
from sadasend import SadaSend

sadasend = SadaSend(os.environ["SADASEND_API_KEY"])

result = sadasend.emails.send(
    from_="noreply@onboarding.sadasend.com",
    to="dana@example.com",
    subject="Hello from Python",
    html="<strong>Transactional email with zero dependencies.</strong>",
)

print(result["status"], result["id"])

Note: from_ has a trailing underscore because from is a reserved Python keyword.


Autonomous AI Agent Safety: The Server-Enforced Moat

Granting an autonomous agent email capability creates a real blast radius. SadaSend solves this with Server-Side Ceilings:

  ┌────────────────────────────────────────────────────────┐
  │  Human Engineer Mints Key in Dashboard / Admin Ops     │
  │  {                                                     │
  │    scopes: ["send", "read"],    # What it may do       │
  │    allowlist: ["@acme.com"],    # Who it may reach     │
  │    rate_limit: "50 / 1h",       # Max rate ceiling     │
  │    mode: "approval"             # Human in the loop    │
  │  }                                                     │
  └──────────────────────────┬─────────────────────────────┘
                             │ Mints sada_agent_sk_...
                             ▼
  ┌────────────────────────────────────────────────────────┐
  │  Autonomous Agent Python Code / LangChain / CrewAI     │
  │  client = SadaSend(os.environ["SADASEND_AGENT_KEY"])   │
  │                                                        │
  │  ❌ Agent CANNOT widen allowlist or edit its scopes    │
  │  ❌ Agent CANNOT mint new keys (session-only endpoint) │
  │  ❌ Agent CANNOT approve its own held mail             │
  └────────────────────────────────────────────────────────┘

The 4 Pillars of Agent Safety

  1. Recipient Allowlists: If an agent attempts to email an unauthorized domain or address, the server intercepts the request before it leaves the outbox, raising RecipientNotAllowlistedError.
  2. Human Approval Mode (mode: "approval"): Keys prefixed with sada_agent_sk_ route outgoing mail to status = "pending_approval". The email is held safely in the Human Approval Queue until a teammate approves it in the dashboard.
  3. Hard Rate Limit Ceilings: Per-key quotas stop recursive runaway loops with RateLimitError (HTTP 429).
  4. Preflight Dry-Runs (dry_run=True): Agents validate rendering, SPF/DKIM verification, and suppression status without dispatching real mail.
import os
from sadasend import SadaSend, RecipientNotAllowlistedError

# Initialized with an agent-scoped key (sada_agent_sk_...)
agent_client = SadaSend(os.environ["SADASEND_AGENT_KEY"])

# 1. Safe Preflight Dry Run: Validates deliverability without sending
preview = agent_client.emails.send(
    from_="noreply@notifications.sadasend.com",
    to="partner@acme.com",
    subject="Quarterly Summary",
    html="<h1>Summary</h1>",
    dry_run=True,
)
print("Dry run validated:", preview["status"])

# 2. Live Send under Human Approval Mode
try:
    result = agent_client.emails.send(
        from_="noreply@notifications.sadasend.com",
        to="partner@acme.com",
        subject="Contract Proposal",
        html="<p>Please review attached contract.</p>",
    )
    if result["status"] == "pending_approval":
        # Safe! The email is held for human sign-off in the dashboard.
        print(f"Message held for human approval. ID: {result['id']}")
except RecipientNotAllowlistedError as e:
    # Model inspects refusal and self-corrects
    print(f"Refused: Recipient {e.recipient} not in allowlist {e.allowlist}")

Agent Provenance Tracking

Track exactly which autonomous agent, pipeline, or model generated each outgoing message:

# Option A: Client-level provenance
client = SadaSend(
    api_key=os.environ["SADASEND_API_KEY"],
    default_agent_name="billing-reconciliation-bot",
    default_source="agent",
    default_channel="sdk",
)

# Option B: Per-request provenance
result = client.emails.send(
    from_="noreply@yourdomain.com",
    to="customer@example.com",
    subject="Monthly Invoice",
    html="<p>Your invoice is ready.</p>",
    agent_name="billing-reconciliation-bot",
    source="agent",
    channel="sdk",
)

Telemetry is visible on the dashboard and via client.account.stats(provenance=True).


Core Features Walkthrough

1. Advanced Transactional Sends (Attachments, CC, BCC, Tags, Headers)

import base64

pdf_data = base64.b64encode(b"%PDF-1.4 sample content...").decode("utf-8")

result = sadasend.emails.send(
    from_="Billing Team <billing@yourdomain.com>",
    to=["customer@example.com"],
    cc=["finance@yourdomain.com"],
    bcc=["archive@yourdomain.com"],
    reply_to="support@yourdomain.com",
    subject="Invoice #INV-2026-08",
    html="<h1>Your Invoice is Ready</h1><p>Thank you for your business!</p>",
    text="Your Invoice is Ready. Thank you for your business!",
    tags=["billing", "invoices"],
    headers={"X-Entity-Ref-ID": "INV-2026-08"},
    attachments=[{
        "filename": "invoice.pdf",
        "content": pdf_data,
        "contentType": "application/pdf",
    }],
    idempotency_key="invoice-send-INV-2026-08",
)

2. High-Efficiency Batch Sends (emails.batch)

Send up to 100 emails in a single atomic HTTP request with RFC 4918 Multi-Status results:

batch = sadasend.emails.batch(emails=[
    {
        "from": "updates@yourdomain.com",
        "to": "alice@example.com",
        "subject": "System Update v2.0",
        "text": "Hi Alice, version 2.0 is live!",
    },
    {
        "from": "updates@yourdomain.com",
        "to": "bob@example.com",
        "subject": "System Update v2.0",
        "text": "Hi Bob, version 2.0 is live!",
    },
])

print(f"Batch processed: {batch["total"]} messages queued.")
for item in batch["results"]:
    print(f"Recipient: {item.get("to")}, Status: {item.get("status")}")

3. Retrieve, Resend, and Cancel Scheduled Emails

# Inspect message delivery state
message = sadasend.emails.get("msg_01hy7q8v")
print(f"Status: {message["status"]}, Delivered at: {message.get("deliveredAt")}")

# Resend an existing message under original parameters
resend = sadasend.emails.resend("msg_01hy7q8v")

# Cancel a scheduled email before dispatch
future_email = sadasend.emails.send(
    from_="reminders@yourdomain.com",
    to="user@example.com",
    subject="Upcoming Appointment",
    text="Don't forget your appointment tomorrow!",
    scheduled_at="2026-11-01T09:00:00Z",
)
cancel_result = sadasend.emails.cancel(future_email["id"])
print("Cancelled status:", cancel_result["status"])

4. Template Rendering & Live Testing (templates)

Store templates server-side with Liquid syntax:

# List available templates
templates = sadasend.templates.list()

# Render template with variables without sending
rendered = sadasend.templates.render(
    id_="tpl_welcome",
    variables={"name": "Alex", "verification_link": "https://example.com/verify?token=123"},
)
print("Preview HTML:", rendered["html"])

# Send a live test email using template
test_send = sadasend.templates.test(
    id_="tpl_welcome",
    to="qa@yourdomain.com",
    variables={"name": "Alex QA"},
)

5. Sending Domains Management & DNS Verification (domains)

# List sending domains
domains = sadasend.domains.list()

# Get required DNS records (SPF, DKIM, DMARC)
dns = sadasend.domains.records("dom_01hy7q8v")
for record in dns["records"]:
    print(f"[{record["type"]}] {record["host"]} -> {record["value"]} ({record["status"]})")

# Trigger live DNS verification check
verification = sadasend.domains.verify("dom_01hy7q8v")
print("Verification status:", verification["status"])

6. Suppression List Management (suppressions)

# Enumerate suppressed recipients
suppressions = sadasend.suppressions.list(limit=50, reason="unsubscribe")

# Manually add address to suppression list
sadasend.suppressions.add(
    address="unsub@example.com",
    reason="manual",
)

7. Account Health & Deliverability Telemetry (account)

# Plan & monthly quota telemetry
acc = sadasend.account.get()
print(f"Plan: {acc['plan']}, Quota: {acc['emailsUsed']} / {acc['emailQuota']}")

# 14-day deliverability metrics
stats = sadasend.account.stats(window="14d")
print(f"Delivered: {stats['delivered']}, Bounce Rate: {stats['bounceRate']}%, Complaint Rate: {stats['complaintRate']}%")

# Daily volume distribution
vol = sadasend.account.volume(window="7d")
for day in vol["days"]:
    print(f"{day['date']}: {day['count']} sent")

Cryptographic HMAC Webhook Verification

Verify inbound delivery, bounce, and complaint webhooks using standard library only (hmac, hashlib).

FastAPI

import os
from fastapi import FastAPI, Request, HTTPException
from sadasend import parse_webhook

app = FastAPI()
WEBHOOK_SECRET = os.environ["SADASEND_WEBHOOK_SECRET"]

@app.post("/webhooks/sadasend")
async def handle_sadasend_webhook(request: Request):
    # CRITICAL: Read raw body bytes. json.loads() roundtrips alter whitespace/key order.
    raw_body = await request.body()
    signature = request.headers.get("sadasend-signature")

    event = parse_webhook(
        secret=WEBHOOK_SECRET,
        body=raw_body,
        header=signature,
    )
    if not event:
        raise HTTPException(status_code=400, detail="Invalid webhook signature")

    event_type = event["type"]
    msg_id = event["data"].get("messageId")
    print(f"Received {event_type} for message {msg_id}")

    return {"received": True}

Flask

import os
from flask import Flask, request, jsonify
from sadasend import parse_webhook

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SADASEND_WEBHOOK_SECRET"]

@app.post("/webhooks/sadasend")
def handle_sadasend_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("sadasend-signature")

    event = parse_webhook(
        secret=WEBHOOK_SECRET,
        body=raw_body,
        header=signature,
    )
    if not event:
        return jsonify({"error": "Invalid webhook signature"}), 400

    return jsonify({"received": True}), 200

Django

import os
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from sadasend import parse_webhook

WEBHOOK_SECRET = os.environ["SADASEND_WEBHOOK_SECRET"]

@csrf_exempt
def sadasend_webhook_view(request):
    if request.method != "POST":
        return HttpResponseBadRequest("POST required")

    raw_body = request.body
    signature = request.headers.get("sadasend-signature")

    event = parse_webhook(
        secret=WEBHOOK_SECRET,
        body=raw_body,
        header=signature,
    )
    if not event:
        return JsonResponse({"error": "Invalid webhook signature"}, status=400)

    return JsonResponse({"received": True})
  • Replay Protection: Timestamps are cryptographically signed with the body and expire after 5 minutes.
  • Timing-Attack Immune: Comparison uses hmac.compare_digest to prevent byte-at-a-time timing attacks.

Typed Exceptions & Error Handling

In Python, an unexamined return value allows a program to silently continue as though mail went out. SadaSend raises specific typed exceptions carrying actionable remediation payloads:

from sadasend import (
    SadaSend,
    DomainNotVerifiedError,
    RecipientNotAllowlistedError,
    RecipientSuppressedError,
    RateLimitError,
)

try:
    result = sadasend.emails.send(
        from_="billing@yourdomain.com",
        to="client@example.com",
        subject="Monthly Invoice",
        html="<p>Invoice attached</p>",
    )
except DomainNotVerifiedError as e:
    # Remediation DNS records are attached directly to the exception
    print("Domain is not verified. Please configure these DNS records:")
    for record in e.records:
        print(f"  {record["type"]} {record["host"]} -> {record["value"]}")
except RecipientNotAllowlistedError as e:
    print(f"Agent key blocked: {e.recipient} is not in allowlist {e.allowlist}")
except RecipientSuppressedError as e:
    print(f"Cannot send to {e.recipient}: suppressed due to previous {e.reason}")
except RateLimitError as e:
    print(f"Rate limited. Obey retry_after_seconds: {e.retry_after_seconds}s")

Exception Hierarchy

Exception Class Trigger Cause Auto-Retried? Attached Payload Attributes
AuthenticationError API key invalid, revoked, or missing No code, status, message
InsufficientScopeError Key lacks required scope (send, read, etc.) No code, status, message
InvalidFromError Malformed or invalid sender email address No code, status, message
DomainNotFoundError Domain not registered on this account No code, status, message
DomainNotVerifiedError Sending domain failed SPF/DKIM verification No records (List of required DNS entries)
RecipientNotAllowlistedError Agent key attempted to email unauthorized address No recipient, allowlist
RecipientSuppressedError Recipient previously unsubscribed or bounced No recipient, reason
IdempotencyKeyReusedError Same idempotency key used with different payload No code, status, message
IdempotencyInProgressError Initial request with this key is still processing Yes code, status, message
RateLimitError Key or account rate limit exceeded (HTTP 429) Yes retry_after_seconds, is_account_throttle
ServerError Transient 5xx upstream server error Yes status, message
ConnectionFailedError Network timeout, socket drop, or DNS failure Yes cause, attempts

Idempotency & Automatic Retries

Every mutating request carries an Idempotency-Key header. The SDK provides:

  1. Zero Duplicate Sends: The exact same Idempotency-Key rides every retry attempt across network drops.
  2. Full Jitter Exponential Backoff: Transient errors (5xx, HTTP 429, socket drops) are retried with randomized jitter to prevent thundering herds.
  3. Respect for Retry-After: The client waits the exact duration specified by the server instead of guessing.
  4. Custom Keys: Pass idempotency_key="order_1234" to guarantee exactly-once delivery across server restarts:
sadasend.emails.send(
    from_="orders@yourdomain.com",
    to="customer@example.com",
    subject="Order Confirmation #1234",
    html="<p>Thank you for your order!</p>",
    idempotency_key=f"order-confirmation-1234",
)

Complete API Reference (All 11 Resources)

Resource Operation Description
client.emails send(...) Send transactional email with attachments, tags, dry run, or approval mode.
client.emails batch(...) Send up to 100 emails in an atomic HTTP batch request.
client.emails get(id_) Retrieve delivery state and timeline for a sent message.
client.emails list(...) Enumerate sent messages with filtering by status, tag, or recipient.
client.emails resend(id_) Resend a previously queued or sent email.
client.emails cancel(id_) Cancel a scheduled email before dispatch.
client.domains list() List all sending domains registered on the account.
client.domains records(id_) Retrieve SPF, DKIM, and DMARC DNS record requirements.
client.domains verify(id_) Run a live internet DNS check to verify domain records.
client.templates list() Enumerate all stored email templates.
client.templates get(id_) Fetch template details including Liquid markup.
client.templates render(id_, ...) Render template against variables without dispatching email.
client.templates test(id_, ...) Send a test email rendering a template.
client.suppressions list(...) Enumerate unsubscribed, bounced, or complained addresses.
client.suppressions add(...) Add an email address to the account suppression list.
client.account get() Retrieve account details, current plan, and monthly send quotas.
client.account stats(...) Deliverability telemetry (delivered, bounce rate, complaint rate).
client.account volume(...) Daily volume trend breakdown across 7d, 14d, or 30d windows.
client.account circuit_breakers() Check active deliverability circuit breaker thresholds.
client.account checklist() Onboarding deliverability checklist items.
client.account staff_access(...) View customer support and staff audit access grants.
client.keys list() Enumerate API keys on the account.
client.keys_approvals list() Enumerate held emails awaiting human approval.
client.support_tickets list(), get(id_) View support tickets and communications.
client.team get() Enumerate team members, owners, and roles.
client.tools list(), check(...) Run free unauthenticated deliverability checks (SPF, DKIM, DMARC, DNS).
client.webhooks list(), deliveries(...) View webhook endpoints and delivery attempt logs.

Native Model Context Protocol (MCP)

Building with Claude Code, Cursor, Windsurf, or custom LLM agent tools? SadaSend features a native Model Context Protocol (MCP) server at POST /mcp.

Add SadaSend directly to your agent config:

{
  "mcpServers": {
    "sadasend": {
      "url": "https://api.sadasend.com/mcp",
      "headers": {
        "Authorization": "Bearer sada_agent_sk_your_key_here"
      }
    }
  }
}

Learn more in our Model Context Protocol Guide.


Frequently Asked Questions (FAQ)

Does sadasend require any external third-party dependencies?

No. sadasend has zero external runtime dependencies. It is built entirely on the Python standard library (urllib.request, hmac, hashlib, json). This means no supply-chain vulnerabilities, zero CVE alerts, zero package conflicts with requests or urllib3, and lightning-fast cold starts in AWS Lambda and Docker.

How does SadaSend prevent autonomous AI agents from sending rogue emails?

SadaSend enforces guardrails on the server:

  1. Recipient Allowlists: Keys can be restricted to specific domains (e.g. @acme.com). Attempts to email unauthorized addresses are blocked with RecipientNotAllowlistedError.
  2. Human Approval Mode: Agent keys (sada_agent_sk_...) can route outgoing emails to status = "pending_approval", requiring a human teammate to click approve in the dashboard.
  3. Hard Rate Limit Ceilings: Prevents runaway recursive agent loops from exhausting budgets.

How does idempotency protect against duplicate sends?

Every request automatically generates a unique Idempotency-Key UUID. If a network connection drops mid-flight and the SDK retries, it sends the exact same key. The SadaSend server detects the duplicate and returns the original response without sending a second email.

Can I use SadaSend with Celery, RQ, or background threads?

Yes. The SadaSend client is stateless and thread-safe. You can initialize a single client instance and share it across threads or instantiate it inside worker tasks.

What Python versions are supported?

Python 3.9, 3.10, 3.11, 3.12, and 3.13 are fully supported and verified in CI.


Canonical Resources


License

MIT © SadaSend

Metadata

Release files for sadasend 0.1.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 sadasend 0.1.0
File Size Uploaded
sadasend-0.1.0.tar.gz 23.4 kB Details

Built distribution (wheel)

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

Total release size: 49.6 kB

Release files / sadasend-0.1.0.tar.gz

Download URL sadasend-0.1.0.tar.gz
Size 23.4 kB
Tags Source
SHA-256 checksum
How to use checksums
b0b43f332bc56393429394df66323997953dd32ac64fabc9ff3a22d3b556c260
BLAKE2b-256 checksum
How to use checksums
0ed3307ae4e86096cd0e9527b1839290e2a0c47a80a94e4dc9109046363c2d36
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 10, 2026.

Transparency log

Release files / sadasend-0.1.0-py3-none-any.whl

Download URL sadasend-0.1.0-py3-none-any.whl
Size 26.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c2dbfc20697d6a00b1777c6fcc79d47ad358b939288911c62bc70a291a8ae68a
BLAKE2b-256 checksum
How to use checksums
4e0cb499e4111fb8cc8dad46f6ad9dfe9ae0a12310468b42142794517dbf8052
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 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.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