sadasend
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 becausefromis 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
- 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. - Human Approval Mode (
mode: "approval"): Keys prefixed withsada_agent_sk_route outgoing mail tostatus = "pending_approval". The email is held safely in the Human Approval Queue until a teammate approves it in the dashboard. - Hard Rate Limit Ceilings: Per-key quotas stop recursive runaway loops with
RateLimitError(HTTP 429). - 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_digestto 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:
- Zero Duplicate Sends: The exact same
Idempotency-Keyrides every retry attempt across network drops. - Full Jitter Exponential Backoff: Transient errors (5xx, HTTP 429, socket drops) are retried with randomized jitter to prevent thundering herds.
- Respect for
Retry-After: The client waits the exact duration specified by the server instead of guessing. - 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:
- Recipient Allowlists: Keys can be restricted to specific domains (e.g.
@acme.com). Attempts to email unauthorized addresses are blocked withRecipientNotAllowlistedError. - Human Approval Mode: Agent keys (
sada_agent_sk_...) can route outgoing emails tostatus = "pending_approval", requiring a human teammate to click approve in the dashboard. - 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
- Official Website: https://sadasend.com
- API Documentation: https://sadasend.com/docs
- Pricing & Plans: https://sadasend.com/pricing
- Model Context Protocol (MCP): https://sadasend.com/docs/mcp
- Developer Support: support@sadasend.com
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)
| File | Size | Uploaded | |
|---|---|---|---|
| sadasend-0.1.0.tar.gz | 23.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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