Stop hand-rolling mail parsers. MultiMail delivers inbound messages as thread-aware API objects so your AI agent can classify, route, and respond without rebuilding email infrastructure.
Raw inbound email is one of the hardest inputs to automate reliably. Messages arrive with inconsistent formatting, nested reply chains, forwarded attachments, and ambiguous intent. Teams that try to wire email into automations end up maintaining brittle MIME parsers, managing IMAP connections, and writing one-off classification logic for every new message type. When a thread spans five replies, context is scattered across quoted blocks. When an attachment matters, it has to be decoded separately. And none of it is structured in a way an AI model can reason about without preprocessing. The result: automation that works in demos and breaks in production whenever a sender formats something differently.
MultiMail receives inbound email on your behalf and converts each message into a structured API object before your agent ever sees it. Webhook events fire immediately on delivery with a normalized payload: sender identity, subject, plain-text and HTML body, attachments as typed metadata, and a thread_id that links every reply in the conversation. Your agent calls check_inbox to poll, or handles the webhook push directly, then uses read_email and get_thread to fetch full context. From there it classifies intent, calls tag_email to annotate the message, and either drafts a reply via reply_email or triggers an external workflow. Under monitored oversight mode, every action your agent takes is visible to your team in real time — no approval gate slows the loop, but nothing happens silently.
MultiMail sends a POST to your registered webhook URL the moment an inbound message arrives. The payload includes the message ID, thread ID, sender, subject, and a normalized body — no MIME parsing required. Register your endpoint in the dashboard under Settings → Webhooks, selecting the inbound_email event type.
Use get_thread with the thread_id from the webhook payload to retrieve the complete conversation history as an ordered array of message objects. Each message includes body text, sender metadata, and attachment references. Passing the full thread to your AI model gives it the context it needs to understand replies, escalations, and prior commitments.
Pass the thread to your model and ask it to classify the message into categories meaningful to your workflow — support request, billing inquiry, feature request, spam, escalation required, and so on. MultiMail does not impose a classification schema; you define the taxonomy that matches your routing logic.
Call tag_email to annotate the message with the classification result and any extracted entities (account ID, order number, urgency level). Tags are queryable, so downstream systems can filter the inbox by classification without re-running the model. Use the classification to trigger the appropriate workflow — open a support ticket, update a CRM record, or queue a human review.
For messages the agent can handle autonomously, call reply_email with the drafted response. Under monitored oversight mode, the reply is sent immediately and your team receives a notification. For messages that require human judgment, call decide_email with action set to escalate, which moves the message into the pending queue and notifies a reviewer without blocking the rest of the inbox.
Under monitored mode, every agent action — reads, tags, replies, escalations — is logged with timestamps and the model's reasoning payload if you pass it. Use list_pending to audit messages the agent escalated, close the loop on reviewer decisions, and feed corrections back into your classification prompt over time.
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.MULTIMAIL_WEBHOOK_SECRET!;
app.post('/webhooks/inbound', async (req, res) => {
const sig = req.headers['x-multimail-signature'] as string;
const body = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).json({ error: 'invalid signature' });
}
const { event, data } = req.body;
if (event !== 'inbound_email') return res.sendStatus(204);
const { message_id, thread_id, from, subject } = data;
console.log(`Inbound: ${message_id} thread=${thread_id} from=${from.email} subject="${subject}"`);
"cm">// Hand off to agent pipeline asynchronously
processInbound(message_id, thread_id).catch(console.error);
res.sendStatus(202);
});
async function processInbound(messageId: string, threadId: string) {
"cm">// Fetch full thread, classify, route — see next samples
}
app.listen(3000);An Express handler that validates the MultiMail webhook signature and extracts the message and thread IDs for downstream processing.
import os
import anthropic
import httpx
MM_API_KEY = os.environ["MULTIMAIL_API_KEY"]
MM_BASE = "https://api.multimail.dev"
headers = {"Authorization": f"Bearer {MM_API_KEY}"}
client = anthropic.Anthropic()
def classify_and_tag(message_id: str, thread_id: str):
"cm"># Fetch full thread
thread_resp = httpx.get(
f"{MM_BASE}/get_thread",
params={"thread_id": thread_id},
headers=headers,
)
thread_resp.raise_for_status()
thread = thread_resp.json()
"cm"># Build context from thread messages
thread_text = "\n---\n".join(
f"From: {m[&"cm">#039;from']['email']}\nDate: {m['date']}\n\n{m['body_plain']}"
for m in thread["messages"]
)
# Classify with Claude
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
system=(
"Classify this email thread into exactly one category: "
"support_request, billing_inquiry, feature_request, spam, or escalation_required. "
"Reply with only the category label."
),
messages=[{"role": "user", "content": thread_text}],
)
intent = resp.content[0].text.strip()
# Write classification back to MultiMail
tag_resp = httpx.post(
f"{MM_BASE}/tag_email",
headers=headers,
json={"message_id": message_id, "tags": [intent, "ai-classified"]},
)
tag_resp.raise_for_status()
print(f"Tagged {message_id} as {intent}")
return intentRetrieves the full conversation thread, sends it to a model for classification, and writes the result back to MultiMail using tag_email.
def respond_or_escalate(message_id: str, thread_id: str, intent: str):
AUTONOMOUS_INTENTS = {"support_request", "billing_inquiry"}
ESCALATE_INTENTS = {"escalation_required"}
if intent in ESCALATE_INTENTS:
resp = httpx.post(
f"{MM_BASE}/decide_email",
headers=headers,
json={
"message_id": message_id,
"action": "escalate",
"reason": f"Classified as {intent} — routing to human reviewer",
},
)
resp.raise_for_status()
print(f"Escalated {message_id}")
return
if intent not in AUTONOMOUS_INTENTS:
print(f"No action configured for intent &"cm">#039;{intent}', skipping")
return
# Fetch thread for reply context
thread = httpx.get(
f"{MM_BASE}/get_thread",
params={"thread_id": thread_id},
headers=headers,
).json()
latest = thread["messages"][-1]
thread_text = "\n---\n".join(m["body_plain"] for m in thread["messages"])
draft_resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
system="Draft a concise, helpful email reply. Do not include a subject line.",
messages=[{"role": "user", "content": thread_text}],
)
reply_body = draft_resp.content[0].text.strip()
send_resp = httpx.post(
f"{MM_BASE}/reply_email",
headers=headers,
json={
"message_id": message_id,
"body": reply_body,
"from_mailbox": "[email protected]",
},
)
send_resp.raise_for_status()
print(f"Replied to {message_id}")Sends an AI-drafted reply for routine messages and escalates to a human reviewer for anything requiring judgment, using reply_email and decide_email.
import time
def poll_and_process(mailbox: str, interval_seconds: int = 30):
"""
Fallback poller for environments where webhook delivery is unreliable.
Prefer webhooks in production — polling introduces latency.
"""
seen: set[str] = set()
while True:
resp = httpx.get(
f"{MM_BASE}/check_inbox",
headers=headers,
params={
"mailbox": mailbox,
"unread_only": True,
"limit": 50,
},
)
resp.raise_for_status()
messages = resp.json().get("messages", [])
for msg in messages:
if msg["message_id"] in seen:
continue
seen.add(msg["message_id"])
intent = classify_and_tag(msg["message_id"], msg["thread_id"])
respond_or_escalate(msg["message_id"], msg["thread_id"], intent)
time.sleep(interval_seconds)Uses check_inbox to pull unread messages when webhooks are unavailable, then feeds each through the classify-and-respond pipeline.
MultiMail normalizes MIME, decodes base64 attachments, strips quoted reply chains, and resolves thread order before your agent ever sees the message. Your code operates on structured JSON, not raw email.
get_thread returns every message in a conversation as an ordered, structured array. Pass it directly to your model. No manual extraction of quoted text, no guessing at reply order across mail clients.
decide_email with action: escalate moves a message into MultiMail's built-in pending queue and notifies your team. You get a human-in-the-loop path without building a separate ticketing integration.
Classification results written via tag_email are stored on the message and queryable via check_inbox filters. CRM integrations, reporting dashboards, and routing rules can all read from the same source of truth without re-running inference.
Under monitored oversight, your agent replies and routes autonomously at full speed. Every action is logged and your team receives notifications. You see what the agent did and why, without approving each step.
Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.