| name | agentmail |
| description | Build email agents using AgentMail API. Use when creating email automation, building AI email agents, setting up webhooks for email notifications, integrating email into AI workflows, or when the user asks about AgentMail, inboxes, or email agents. |
AgentMail
AgentMail is an API-first email platform for AI agents. Unlike traditional email services, it's designed for two-way conversations, allowing agents to send, receive, and reply to emails autonomously.
Quick Start
Installation
pip install agentmail
npm install agentmail
Initialize Client
from agentmail import AgentMail
client = AgentMail()
import { AgentMailClient } from "agentmail";
const client = new AgentMailClient();
Create Inbox and Send Email
inbox = client.inboxes.create(
username="my-agent",
client_id="my-agent-inbox"
)
print(f"Created: {inbox.inbox_id}")
client.inboxes.messages.send(
inbox_id=inbox.inbox_id,
to=["user@example.com"],
subject="Hello from my agent",
text="Plain text version",
html="<p>HTML version</p>",
labels=["outreach"]
)
const inbox = await client.inboxes.create({
username: "my-agent",
clientId: "my-agent-inbox"
});
await client.inboxes.messages.send(inbox.inboxId, {
to: ["user@example.com"],
subject: "Hello from my agent",
text: "Plain text version",
html: "<p>HTML version</p>",
labels: ["outreach"]
});
Resource Hierarchy
Organization (top-level container)
└── Pod (optional, for multi-tenancy)
└── Inbox (email account, e.g., agent@agentmail.to)
└── Thread (conversation, auto-created)
└── Message (individual email)
└── Attachment (files)
Core Concepts
| Resource | Purpose |
|---|
| Organization | Top-level container for all resources |
| Inbox | Email account (e.g., agent@agentmail.to) |
| Message | Individual email with text, html, attachments |
| Thread | Conversation grouping (auto-created) |
| Webhook | Event notifications via HTTP POST |
| WebSocket | Persistent bidirectional connection |
| Pod | Multi-tenant isolation (optional) |
| Domain | Custom domain with SPF/DKIM/DMARC |
| Draft | Unsent message for review |
| Labels | String tags for filtering and state management |
Common Workflows
1. Reply to Email
client.inboxes.messages.reply(
inbox_id="agent@agentmail.to",
message_id="msg_xxx",
text="Thanks for your message!",
html="<p>Thanks for your message!</p>"
)
await client.inboxes.messages.reply("agent@agentmail.to", "msg_xxx", {
text: "Thanks for your message!",
html: "<p>Thanks for your message!</p>"
});
2. List Messages with Label Filter
messages = client.inboxes.messages.list(
inbox_id="agent@agentmail.to",
labels=["unread", "important"]
)
for msg in messages.messages:
print(f"{msg.subject} from {msg.from_}")
const messages = await client.inboxes.messages.list("agent@agentmail.to", {
labels: ["unread", "important"]
});
for (const msg of messages.messages) {
console.log(`${msg.subject} from ${msg.from}`);
}
3. Update Labels on Message
client.inboxes.messages.update(
inbox_id="agent@agentmail.to",
message_id="msg_xxx",
add_labels=["processed"],
remove_labels=["unread"]
)
await client.inboxes.messages.update("agent@agentmail.to", "msg_xxx", {
addLabels: ["processed"],
removeLabels: ["unread"]
});
4. List Threads Org-Wide
all_threads = client.threads.list()
inbox_threads = client.inboxes.threads.list(inbox_id="agent@agentmail.to")
const allThreads = await client.threads.list();
const inboxThreads = await client.inboxes.threads.list("agent@agentmail.to");
5. Send Attachment
import base64
with open("report.pdf", "rb") as f:
content = base64.b64encode(f.read()).decode()
client.inboxes.messages.send(
inbox_id="agent@agentmail.to",
to=["user@example.com"],
subject="Report attached",
text="Please see attached.",
attachments=[{
"content": content,
"filename": "report.pdf",
"content_type": "application/pdf"
}]
)
import * as fs from "fs";
const content = fs.readFileSync("report.pdf").toString("base64");
await client.inboxes.messages.send("agent@agentmail.to", {
to: ["user@example.com"],
subject: "Report attached",
text: "Please see attached.",
attachments: [{
content,
filename: "report.pdf",
contentType: "application/pdf"
}]
});
Webhook Setup (Flask + ngrok)
Webhooks notify your agent in real-time when events occur (e.g., new email received).
Complete Example
import os
from threading import Thread
from flask import Flask, request, Response
import ngrok
from agentmail import AgentMail
app = Flask(__name__)
client = AgentMail()
port = 8080
listener = ngrok.forward(port, authtoken_from_env=True)
webhook_url = f"{listener.url()}/webhooks"
client.inboxes.create(username="webhook-agent", client_id="webhook-agent-inbox")
client.webhooks.create(
url=webhook_url,
event_types=["message.received"],
client_id="webhook-agent-webhook"
)
@app.route("/webhooks", methods=["POST"])
def receive_webhook():
Thread(target=process_webhook, args=(request.json,)).start()
return Response(status=200)
def process_webhook(payload):
event_type = payload["event_type"]
if event_type == "message.received":
message = payload["message"]
print(f"New email from {message['from']}: {message['subject']}")
client.inboxes.messages.reply(
inbox_id=message["inbox_id"],
message_id=message["message_id"],
text="Thanks for your email! I'll get back to you soon."
)
if __name__ == "__main__":
print(f"Webhook URL: {webhook_url}")
app.run(port=port)
Webhook Event Types
message.received - New email arrived (includes full Thread + Message data)
message.sent - Email was sent
message.delivered - Email was delivered to recipient's server
message.bounced - Email failed to deliver
message.complained - Recipient marked as spam
message.rejected - Email rejected before sending
domain.verified - Custom domain verified
See references/webhook-events.md for payload structures.
WebSocket Real-time Events
WebSockets provide real-time events without needing a public URL.
Async Pattern
import asyncio
from agentmail import AsyncAgentMail, Subscribe, MessageReceivedEvent
client = AsyncAgentMail()
async def main():
async with client.websockets.connect() as socket:
await socket.send_subscribe(Subscribe(
inbox_ids=["agent@agentmail.to"]
))
async for event in socket:
if isinstance(event, MessageReceivedEvent):
print(f"New email: {event.message.subject}")
asyncio.run(main())
const socket = await client.websockets.connect();
socket.on("open", () => {
socket.sendSubscribe({
type: "subscribe",
inboxIds: ["agent@agentmail.to"]
});
});
socket.on("message", (event) => {
if (event.type === "message_received") {
console.log(`New email: ${event.message.subject}`);
}
});
AI Agent Integration
Use agentmail-toolkit to give AI agents email capabilities.
Installation
pip install agentmail-toolkit
OpenAI Agents
from agentmail import AgentMail
from agentmail_toolkit.openai import AgentMailToolkit
from agents import Agent, Runner
client = AgentMail()
toolkit = AgentMailToolkit(client)
agent = Agent(
name="Email Agent",
instructions=f"""You are an email agent. Your inbox is agent@agentmail.to.
You can send, receive, and reply to emails.""",
tools=toolkit.get_tools()
)
response = Runner.run(agent, [{"role": "user", "content": "Send a hello email to user@example.com"}])
Langchain
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from agentmail_toolkit.langchain import AgentMailToolkit
agent = create_react_agent(
model=ChatOpenAI(model="gpt-4o"),
tools=AgentMailToolkit().get_tools()
)
Best Practices
Idempotency
Always use client_id on create operations to prevent duplicates:
inbox = client.inboxes.create(
username="my-agent",
client_id="user-123-primary-inbox"
)
webhook = client.webhooks.create(
url="https://example.com/webhooks",
event_types=["message.received"],
client_id="user-123-webhook"
)
Webhook Handling
Always return 200 immediately and process in background:
@app.route("/webhooks", methods=["POST"])
def webhook():
Thread(target=process, args=(request.json,)).start()
return Response(status=200)
Reply Extraction
Use extracted_text / extracted_html fields for clean reply content (removes quoted text):
message = client.inboxes.messages.get(inbox_id, message_id)
clean_reply = message.extracted_text
full_email = message.text
Or use Talon library for more control:
from talon import quotations
clean = quotations.extract_from_plain(email_text)
Critical Gotchas
-
Bounced/complained addresses are permanently blocked - AgentMail prevents sending to them to protect your reputation
-
Keep bounce rate < 4% - Or your account goes under review
-
AWS Route 53 DKIM records - Must split into two quoted strings with NO space:
Correct: "first-part""second-part"
Wrong: "first-part" "second-part" (space breaks it)
-
Only one SPF record per domain - Merge multiple services:
v=spf1 include:spf.agentmail.to include:other.com ~all
-
message.received is the only webhook with full Thread + Message data - Other events have minimal metadata
-
Pods cannot be deleted with existing resources - Delete all inboxes/domains in the pod first
-
Inboxes cannot be moved between pods - Create new inbox in target pod
IMAP/SMTP Access
For email client integration:
| Protocol | Host | Port | Auth |
|---|
| IMAP | imap.agentmail.to | 993 (SSL) | inbox email + API key |
| SMTP | smtp.agentmail.to | 465 (SSL) | inbox email + API key |
Additional Resources