Send Transactional Emails from Python with MailAnvil MCP — Guide for AI SaaS Builders
Python powers the AI stack. LangChain, LlamaIndex, FastAPI, Celery — every AI SaaS has a Python backend somewhere. And every SaaS needs to send emails: welcome emails, OTPs, invoices, notifications.
This guide covers three ways to send transactional emails from Python with MailAnvil:
- REST API — direct HTTPS calls. Works with any Python framework.
- MCP client — for AI agents (Claude Code, Codex) building your Python app.
- Django/FastAPI integration — production patterns with async, retries, and templates.
Let's go.
Prerequisites
pip install requests httpx
A MailAnvil account with a verified domain. Sign up at mailanvil.com — free tier covers 500 emails/month.
1. REST API — The direct approach
Every Python framework can call MailAnvil's REST API. No SDK needed — just HTTPS.
import requests
import json
MAILANVIL_API = "https://api.mailanvil.com/v1/send"
API_KEY = "re_..." # from your dashboard
def send_email(to: list[str], subject: str, html: str, from_addr: str = None):
"""Send a transactional email via MailAnvil."""
payload = {
"from": from_addr or "[email protected]",
"to": to,
"subject": subject,
"html": html,
}
resp = requests.post(
MAILANVIL_API,
json=payload,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
)
resp.raise_for_status()
return resp.json() # {"id": "em_01J...", "state": "queued"}
# Usage
result = send_email(
to=["[email protected]"],
subject="Welcome to our app!",
html="<h1>Welcome!</h1><p>Thanks for signing up.</p>",
)
print(f"Email queued: {result['id']}")
MailAnvil returns a 202 Accepted immediately. The email is queued, processed asynchronously, and delivery status is tracked via webhooks or the GET /v1/emails/:id endpoint.
With httpx (async)
import httpx
import asyncio
async def send_email_async(to, subject, html):
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.mailanvil.com/v1/send",
json={"from": "[email protected]", "to": to, "subject": subject, "html": html},
headers={"Authorization": "Bearer re_...", "Content-Type": "application/json"},
)
resp.raise_for_status()
return resp.json()
# In your async handler
result = asyncio.run(send_email_async(["[email protected]"], "Hello!", "<p>Test</p>"))
2. MCP client — For AI agents building your app
If you're using Claude Code, Cursor, or Codex to build your Python app, the MCP approach is even simpler. The AI agent discovers MailAnvil's MCP server and calls its tools directly — no REST endpoints to remember, no headers to construct.
Setup
Add to your claude.json or MCP config:
{
"mcpServers": {
"mailanvil": {
"url": "https://mcp.mailanvil.com/mcp",
"headers": {
"Authorization": "Bearer re_..."
}
}
}
}
What the AI agent sees
When your AI agent connects to the MCP server, it discovers these tools:
| Tool | Description | Key args |
|---|---|---|
send_email |
Send transactional email | from, to[], subject, html |
get_email_logs |
Check delivery status | limit, offset |
list_domains |
List verified domains | none |
list_templates |
List templates | none |
get_usage_stats |
Billing usage | none |
Example: AI agent builds a Flask email endpoint
Tell Claude Code or Cursor:
"Build a Flask app with a
/send-welcomeendpoint that uses MailAnvil MCP to send a welcome email."
The agent will discover the MCP tools and generate working code — no REST docs needed. It calls send_email() as a native tool, and the MCP server handles the HTTP layer.
From Python MCP client
If you want to call MCP from Python directly (building an AI agent that sends email):
pip install mcp httpx
import json
import httpx
class MailAnvilMCP:
"""Minimal MCP client for MailAnvil."""
def __init__(self, api_key: str):
self.url = "https://mcp.mailanvil.com/mcp"
self.headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
async def send_email(self, to: list[str], subject: str, html: str, from_addr: str = None):
"""Call the send_email tool via MCP."""
payload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "send_email",
"arguments": {
"to": to,
"subject": subject,
"html": html,
"from": from_addr or "[email protected]",
},
},
"id": 1,
}
async with httpx.AsyncClient() as client:
resp = await client.post(self.url, json=payload, headers=self.headers)
result = resp.json()
return result.get("result", {})
# Usage in an AI agent pipeline
mcp = MailAnvilMCP("re_...")
result = await mcp.send_email(["[email protected]"], "AI Agent says hi!", "<p>Built by an AI agent.</p>")
3. Django integration
For Django projects, add a utility module:
# mailanvil.py — Drop this into your Django app
import requests
from django.conf import settings
def send_transactional_email(to, subject, html, from_email=None):
"""Send email via MailAnvil from Django."""
resp = requests.post(
f"{settings.MAILANVIL_API_BASE}/v1/send",
json={
"from": from_email or settings.DEFAULT_FROM_EMAIL,
"to": [to] if isinstance(to, str) else to,
"subject": subject,
"html": html,
},
headers={
"Authorization": f"Bearer {settings.MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
)
resp.raise_for_status()
return resp.json()["id"]
In your settings.py:
MAILANVIL_API_BASE = "https://api.mailanvil.com"
MAILANVIL_API_KEY = "re_..."
DEFAULT_FROM_EMAIL = "[email protected]"
Then in your views:
from .mailanvil import send_transactional_email
def signup_view(request):
# ... process signup ...
email_id = send_transactional_email(
to=user.email,
subject="Welcome to MyApp!",
html=render_to_string("emails/welcome.html", {"user": user}),
)
return JsonResponse({"status": "ok", "email_id": email_id})
4. FastAPI + Celery (production pattern)
For high-volume transactional email (10K+/month), offload to Celery:
# tasks.py
from celery import Celery
import httpx
celery_app = Celery("tasks", broker="redis://localhost:6379/0")
@celery_app.task(bind=True, max_retries=3, default_retry_delay=60)
def send_email_task(self, to, subject, html, from_addr):
"""Celery task for reliable email delivery."""
payload = {
"from": from_addr,
"to": [to] if isinstance(to, str) else to,
"subject": subject,
"html": html,
}
headers = {
"Authorization": "Bearer re_...",
"Content-Type": "application/json",
}
resp = httpx.post(
"https://api.mailanvil.com/v1/send",
json=payload,
headers=headers,
)
if resp.status_code == 429: # rate limited — retry
raise self.retry()
resp.raise_for_status()
return resp.json()["id"]
# main.py — FastAPI
from fastapi import FastAPI
from .tasks import send_email_task
app = FastAPI()
@app.post("/signup")
async def signup(email: str):
# ... create user ...
send_email_task.delay(
to=email,
subject="Welcome!",
html="<h1>Thanks for joining</h1>",
from_addr="[email protected]",
)
return {"status": "queued"}
Best practices
DKIM verification (INV-1)
MailAnvil blocks sends from unverified domains. Before sending, verify your domain:
resp = requests.post(
"https://api.mailanvil.com/v1/domains",
json={"domain": "yourdomain.com"},
headers={"Authorization": "Bearer re_..."},
)
If your domain is on Cloudflare DNS, MailAnvil's auto-DKIM sets up DKIM records automatically. Otherwise, copy the DNS records from the response to your DNS provider.
Retry on 429
MailAnvil rate-limits at 10 req/s per key by default. Handle 429 with exponential backoff:
import time
def send_with_retry(payload, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(MAILANVIL_API, json=payload, headers=headers)
if resp.status_code == 429:
wait = 2 ** attempt
time.sleep(wait)
continue
resp.raise_for_status()
return resp.json()
raise Exception("Max retries exceeded")
Check delivery status
After sending, poll for delivery: GET /v1/emails/{id} returns the email state (queued, delivered, bounced, suppressed, failed).
def check_delivery(email_id):
resp = requests.get(
f"https://api.mailanvil.com/v1/emails/{email_id}",
headers={"Authorization": "Bearer re_..."},
)
return resp.json()["state"]
Or set up webhooks for real-time delivery events — no polling needed.
Why Python devs choose MailAnvil
| Feature | MailAnvil | Competitors |
|---|---|---|
| MCP-native | ✅ Built-in MCP server | ❌ REST-only or external MCP |
| IDR pricing | ✅ Rp 149rb/10K emails | ❌ $10-35 USD |
| QRIS/GoPay | ✅ Local payment | ❌ Credit card only |
| Bahasa docs | ✅ Full ID docs | ❌ English only |
| CF auto-DKIM | ✅ One-click for CF DNS | ❌ Manual setup |
| Workers-native | ✅ Zero cold start | ❌ VM-based |
Next steps
- Get your free API key — 500 emails/month, no credit card
- MCP setup docs — configure for Claude Code, Cursor, Codex
- API reference — full endpoint documentation
- Python template repo — starter code for Django, FastAPI, Flask
MailAnvil — Transactional Email API for Indonesian developers. IDR pricing, QRIS/GoPay, Bahasa docs. Built on Cloudflare Workers + AWS SES.