Zero dependencies by design
Hand-written, not generated. Typed template variables, a distinct error class per failure mode, and no required configuration beyond the key.
Every dependency in an email client is an attack vector, a bundle bloater, and an eventual CVE notice. SadaSend SDKs declare zero external runtime dependencies.
TypeScript uses native global fetch and WebCrypto across Node 18+, Bun, Deno, and Edge runtimes. Python uses standard library urllib.request and hmac.
The autonomous agent moat: server-enforced ceilings
If an agent could pass its own scopes or allowlists in code (new SadaSend({ allowlist: ... })), a prompt injection or hallucinated loop could simply rewrite the client configuration.
In SadaSend, the ceiling is set once when the key is minted in the dashboard or via human session. The agent receives only the key (sada_agent_sk_…) and has zero ability to widen its permissions, increase rate limits, or bypass approval mode.
- Recipient Allowlists: Out-of-bounds sends are rejected immediately before reaching the mail provider.
- Approval Mode: Outbound sends are placed into
pending_approvalfor human sign-off in the dashboard. - Key Expiration & CIDR IP Guardrails: Hard ISO expiration dates (
expires_at) and network IP restrictions (ip_allowlist) ensure credentials in agent runtimes are inert outside designated VPCs or past scheduled job windows. - Hard Rate Limits: Throttles runaway agent loops with HTTP 429.
- Preflight Dry Runs:
dryRun: truelets agents validate templates, schemas, and deliverability with zero cost.
TypeScript SDK
Type-safe discriminated unions (Result<T, E>), automatic retry with full jitter, built-in idempotency, and type narrowing.
// bun add sadasend (or npm i sadasend)
import { SadaSend, RecipientNotAllowlistedError } from 'sadasend';
const sadasend = new SadaSend(process.env.SADASEND_API_KEY!);
// 1. Transactional send with cc, bcc, attachments, tags, provenance & idempotency
const res = await sadasend.emails.send({
from: 'billing@yourdomain.com',
to: 'customer@example.com',
cc: ['finance@example.com'],
bcc: ['audit@yourdomain.com'],
reply_to: 'support@yourdomain.com',
subject: 'Monthly Statement',
html: '<h1>Your Statement</h1><p>Please find attached.</p>',
tags: ['billing', 'monthly'],
headers: {
'X-SadaSend-Agent': 'Invoice Automation',
'X-SadaSend-Source': 'automation',
},
attachments: [{
filename: 'statement.pdf',
content: Buffer.from('%PDF-1.4...').toString('base64'),
contentType: 'application/pdf',
}],
idempotencyKey: 'stmt-2026-09-user-123',
});
// 2. Refusals are results, not exceptions
if (!res.ok) {
if (res.error instanceof RecipientNotAllowlistedError) {
console.error('Allowlist violation. Allowed:', res.error.allowlist);
}
} else {
// res.status is 'queued' for live keys, or 'pending_approval' for agent keys
console.log(res.status, res.id);
}
// 3. Batch sends (up to 100 per call, RFC 4918 Multi-Status)
const batch = await sadasend.emails.batch([
{ from: 'updates@yourdomain.com', to: 'alice@example.com', text: 'Hi Alice' },
{ from: 'updates@yourdomain.com', to: 'bob@example.com', text: 'Hi Bob' },
]);Python SDK
Generated from the published OpenAPI specification, so it stays in step with the API automatically.
Clean standard-library client matching 100% of the public API surface with typed exceptions.
# pip install sadasend
import os, base64
from sadasend import SadaSend, RecipientNotAllowlistedError
sadasend = SadaSend(os.environ["SADASEND_API_KEY"])
try:
# 1. Transactional send with cc, bcc, attachments, tags, and provenance
pdf_b64 = base64.b64encode(b"%PDF-1.4...").decode("utf-8")
result = sadasend.emails.send(
from_="billing@yourdomain.com",
to="customer@example.com",
cc=["finance@example.com"],
bcc=["audit@yourdomain.com"],
reply_to="support@yourdomain.com",
subject="Monthly Statement",
html="<h1>Your Statement</h1>",
tags=["billing", "monthly"],
headers={
"X-SadaSend-Agent": "Billing Worker",
"X-SadaSend-Source": "backend",
},
attachments=[{
"filename": "statement.pdf",
"content": pdf_b64,
"contentType": "application/pdf",
}],
idempotency_key="stmt-2026-09-user-123",
)
print(result["status"], result["id"])
except RecipientNotAllowlistedError as e:
# Typed exception with remediation context attached
print(f"Refused: {e.recipient} not in allowlist {e.allowlist}")
# 2. Preflight dry run (zero cost validation)
dry_run = sadasend.emails.send(
from_="billing@yourdomain.com",
to="customer@example.com",
subject="Validation",
text="Test",
dry_run=True,
)What both ship with
- Automatic retry with exponential backoff on 5xx and 429, respecting
Retry-After. - Idempotency keys generated by default, so a retried send is never a duplicate email.
- A webhook signature verifier — everyone needs one, and getting it wrong is a security hole.
- Errors that name the fix.
DomainNotVerifiedErrorcarries the missing records.
Complete resource reference
| Resource | Method | Scope | Description |
|---|---|---|---|
| emails | send(...) | send | Single send with attachments, tags, dry-run or approval mode |
| emails | batch(...) | send | High-throughput atomic batch send up to 100 recipients |
| emails | get(id) | read | Delivery status and event trail without leaking message body |
| emails | resend(id) | send | Resend an email under its original issuing key |
| emails | cancel(id) | send | Cancel a scheduled or pending email before dispatch |
| templates | list() | read | List versioned Liquid templates on this account |
| templates | get(id) | read | Fetch stored template body, schema, and version history |
| templates | render(...) | read | Render a template against variable values without sending |
| templates | test(id, ...) | send | Dispatch test send with custom JSON variables to verified recipient |
| domains | list() | read | List registered sending domains and statuses |
| domains | records(id) | read | Fetch required SPF, DKIM, and DMARC DNS records |
| domains | verify(id) | domains | Trigger live DNS query to verify records on internet |
| domains | restore(id) | domains | Restore a previously removed domain without regenerating DKIM DNS keys |
| webhooks | list() | read | List registered webhook delivery endpoints and statuses |
| webhooks | create(...) | webhooks | Register signed webhook endpoint with event subscriptions |
| webhooks | simulate(...) | read | Simulate webhook payload delivery to a local or remote URL with HMAC signing |
| suppressions | check(addr) | read | Check if address is suppressed (bounce, complaint, unsubscribe) |
| suppressions | add(...) | send | Add an address manually to account suppression list |
| account | get() | read | Retrieve account plan, email quota, and sending utilization |
| account | volume(...) | read | Query daily sending volume array for sparkline charts over a date window |
| account | stats(...) | read | Query bounce rates, complaint rates, and sparkline volume. Pass provenance=true for byChannel, bySource, and topAgents breakdown matrices |
Constant-time HMAC webhook verification
Timing-safe, replay-protected webhook parsing is built directly into both SDKs (parseWebhook in TypeScript, parse_webhook in Python).
Signatures are compared in constant time with a 5-minute replay tolerance window.
import { parseWebhook } from 'sadasend/webhooks';
// In Next.js / Remix / Express / Fastify / Cloudflare:
const raw = await req.text(); // Raw body string, NOT parsed JSON!
const event = await parseWebhook(process.env.WEBHOOK_SECRET!, raw, req.headers.get('sadasend-signature'));
if (!event) return new Response('Bad signature', { status: 400 });
console.log(event.type, event.data.messageId);SMTP Relay
If you already have a working mailer, you do not need an SDK at all. Point it at the relay and change credentials rather than code.
If you already use an existing mail framework (Nodemailer, Django, Laravel, Postfix), point your SMTP transport to SadaSend with zero code rewrites.
host: smtp.sadasend.com
port: 587
security: STARTTLS
user: sadasend
pass: $SADASEND_API_KEY