How to send email
from a Python AI agent.
The shortest working recipe for making a Python agent send email from its own address — no SMTP setup, no OAuth, just an API key.
Most Python agent tutorials for email recommend smtplib against Gmail or a transactional provider. Both work; neither gives the agent its own identity. This recipe uses Loomal, which hands you a DKIM-signed mailbox per agent via a single REST endpoint.
The whole setup is one environment variable and one HTTP POST. No SMTP credentials, no OAuth flow, no email provider account beyond Loomal itself.
1. Get an API key
Sign up at console.loomal.ai, create an identity, and copy the API key (loid-...). You now own an email address like agent-x8k2m@loomal.ai — every message you send will be from this address, DKIM-signed.
export LOOMAL_API_KEY="loid-your-api-key"2. Send with the requests library
If you're already using requests, this is the minimal working version. POST to /v0/messages/send with to, subject, and text. Loomal returns a messageId and threadId you can keep for correlation.
import os, requests
res = requests.post(
"https://api.loomal.ai/v0/messages/send",
headers={
"Authorization": f"Bearer {os.environ['LOOMAL_API_KEY']}",
"Content-Type": "application/json",
},
json={
"to": ["alice@example.com"],
"subject": "Hello from your agent",
"text": "This is a DKIM-signed message sent by an AI agent using Loomal.",
},
timeout=10,
)
res.raise_for_status()
print(res.json()["messageId"])3. Async version with httpx
For async agents (the common case for anything using modern LLM SDKs), use httpx. The request shape is identical; only the client changes.
import os, asyncio, httpx
async def send(to: str, subject: str, text: str) -> str:
async with httpx.AsyncClient(timeout=10) as client:
res = await client.post(
"https://api.loomal.ai/v0/messages/send",
headers={"Authorization": f"Bearer {os.environ['LOOMAL_API_KEY']}"},
json={"to": [to], "subject": subject, "text": text},
)
res.raise_for_status()
return res.json()["messageId"]
asyncio.run(send("alice@example.com", "Hello", "From an async agent"))4. Expose it as a tool to your LLM
For an agent that decides when to send, wrap the function as a tool. The shape depends on your framework — here's the pattern for the OpenAI Agents SDK. For framework-specific guides, see the Guides hub.
from agents import Agent, function_tool
@function_tool
def send_email(to: str, subject: str, text: str) -> str:
"""Send an email from the agent's own address."""
return asyncio.run(send(to, subject, text))
agent = Agent(
name="outreach",
instructions="You write and send follow-up emails to leads.",
tools=[send_email],
)5. Use the MCP server instead of writing tools
If your agent framework supports MCP (Claude Agent SDK, OpenAI Agents SDK, LangChain, Mastra, etc.), skip the wrapper and register @loomal/mcp as an MCP server. The agent gets mail.send, mail.reply, and the rest as typed tools with no Python wrapper code.
FAQ
Do I need to configure SMTP or DNS?
No. Loomal handles DKIM, SPF, and DMARC for the default domain. If you want to send from your own domain, configure those records once and your identities sign under your brand.
How do I send an attachment?
Add an attachments array to the JSON body, each with filename, contentType, and base64-encoded content. The REST endpoint accepts up to 25MB total per message.
Can I use the async version inside an async agent without asyncio.run?
Yes — call await send(...) directly from within an async context. The asyncio.run wrapper is only for top-level sync entry points.
Related reading
Last updated: 2026-04-15