← Back to all posts
2026-07-30 · MailAnvil Team

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:

  1. REST API — direct HTTPS calls. Works with any Python framework.
  2. MCP client — for AI agents (Claude Code, Codex) building your Python app.
  3. 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-welcome endpoint 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


MailAnvil — Transactional Email API for Indonesian developers. IDR pricing, QRIS/GoPay, Bahasa docs. Built on Cloudflare Workers + AWS SES.