Email is one of the most useful communication channels for AI agents — but most frameworks treat it as an afterthought. IMAP is a nightmare. SMTP requires MIME construction. And per-inbox SaaS pricing kills the unit economics of high-volume agent workloads.
Here’s how to give your agents real email addresses and handle the full send/receive/webhook lifecycle.
The architecture
A solid email-capable agent needs four things:
- An inbox — a real email address that can send and receive
- An outbound path — reliable SMTP or API-based sending
- An inbound path — a way to receive and store incoming messages
- Event notification — real-time notification when something arrives
IMAP polling handles #3 badly. It’s slow, resource-intensive, and not designed for programmatic use. Webhooks are the right primitive — push notification the moment an email arrives.
Step 1: Provision an inbox
import httpx
client = httpx.Client(
base_url="https://api.emails4agents.com",
headers={"X-API-Key": "e4a_your_key"}
)
# Create inbox
inbox = client.post("/v1/inboxes", json={
"username": "research-agent",
"domain_id": "your-domain-uuid",
"display_name": "Research Agent"
}).json()
print(inbox["address"]) # [email protected]
Step 2: Register a webhook
webhook = client.post("/v1/webhooks", json={
"url": "https://your-agent-server.com/webhooks/email",
"events": ["message.received", "message.bounced"]
}).json()
Step 3: Handle incoming mail
Your webhook endpoint receives a POST with the full message:
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/webhooks/email")
async def handle_email(request: Request):
event = await request.json()
if event["event"] == "message.received":
message = event["data"]
# Route to your agent
await process_with_agent(
inbox_id=message["inbox_id"],
thread_id=message["thread_id"],
from_address=message["from_address"],
subject=message["subject"],
body=message["text_body"],
classification=message.get("classification"),
)
return {"ok": True}
Step 4: Send a reply
async def process_with_agent(inbox_id, thread_id, from_address, subject, body, classification):
# Your agent logic here
response_text = await your_llm(body)
# Find the original message to reply to
messages = client.get(f"/v1/threads/{thread_id}").json()
original_id = messages[0]["id"]
# Reply in-thread
client.post(f"/v1/messages/{original_id}/reply", json={
"text": response_text
})
Using classification to route messages
If you’ve configured the AI classifier on the inbox, you get an intent label on every inbound message before your webhook fires:
if classification == "support_request":
await route_to_support_agent(message)
elif classification == "sales_lead":
await route_to_sales_agent(message)
elif classification == "spam":
pass # ignore
The classifier runs via OpenRouter so you can pick any model — fast/cheap for volume, powerful for complex routing.
What to avoid
- Don’t poll — use webhooks for real-time notification
- Don’t parse raw MIME — the API returns structured JSON fields for every message component
- Don’t use shared inboxes — give each agent its own address for clean threading and attribution
- Don’t skip idempotency keys — if your agent retries send operations, use
idempotency_keyto prevent duplicate emails
See the full API reference for all available endpoints.