Build email agents using AgentMail API...
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.
# Python
pip install agentmail
# Node.js
npm install agentmail
from agentmail import AgentMail
client = AgentMail() # Uses AGENTMAIL_API_KEY from environment
import { AgentMailClient } from "agentmail";
const client = new AgentMailClient(); // Uses AGENTMAIL_API_KEY from environment
# Create inbox (use client_id for idempotency)
inbox = client.inboxes.create(
username="my-agent",
client_id="my-agent-inbox"
)
print(f"Created: {inbox.inbox_id}") # e.g., my-agent@agentmail.to
# Send email (always include both text and html)
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"]
});
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)
| 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 |
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>"
});
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}`);
}
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"]
});
# Query all threads across all inboxes (for supervisor agents)
all_threads = client.threads.list()
# Or per inbox
inbox_threads = client.inboxes.threads.list(inbox_id="agent@agentmail.to")
// Query all threads across all inboxes (for supervisor agents)
const allThreads = await client.threads.list();
// Or per inbox
const inboxThreads = await client.inboxes.threads.list("agent@agentmail.to");
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"
}]
});
Webhooks notify your agent in real-time when events occur (e.g., new email received).
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
# Start ngrok tunnel
listener = ngrok.forward(port, authtoken_from_env=True)
webhook_url = f"{listener.url()}/webhooks"
# Create inbox and webhook idempotently
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():
# Return 200 immediately, process in background
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']}")
# Reply to the message
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)
message.received - New email arrived (includes full Thread + Message data)message.sent - Email was sentmessage.delivered - Email was delivered to recipient's servermessage.bounced - Email failed to delivermessage.complained - Recipient marked as spammessage.rejected - Email rejected before sendingdomain.verified - Custom domain verifiedSee references/webhook-events.md for payload structures.
WebSockets provide real-time events without needing a public URL.
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}`);
}
});
Use agentmail-toolkit to give AI agents email capabilities.
pip install agentmail-toolkit
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"}])
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()
)
Always use client_id on create operations to prevent duplicates:
# Safe to run multiple times - won't create 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"
)
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) # Return immediately!
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 # Just the new content
full_email = message.text # Includes quoted replies
Or use Talon library for more control:
from talon import quotations
clean = quotations.extract_from_plain(email_text)
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
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 |