How to Send Email From AI Agent: Python and TypeScript
Send email from AI agent code in Python or TypeScript: create a mailbox, one POST, plus the deliverability basics agents get wrong.
John Joubert
Founder, Robotomail

Table of contents
The short answer to how to send email from AI agent code is two calls: create a mailbox the agent owns, then POST a message to that mailbox. No SMTP handshake, no shared human inbox, no OAuth consent screen. In Python or TypeScript it is roughly fifteen lines. The part that takes longer than the send is getting identity and replies right, which is where most agent email projects fall over.
This post walks the fastest correct path, then covers the deliverability mistakes we see agents make most often. If you want a vendor comparison instead, we keep a separate roundup of the best email APIs for AI agents.
Why agents need their own mailbox, not just a sender
A bulk-sending API can push a message out. That is half a conversation. An agent that emails a vendor about order A-4521 needs to read the reply, match it to the thread, and act. If the reply lands in a human's Gmail, or nowhere at all, the workflow stops.
So the unit of setup is a mailbox: a real address with an inbox, threading, and an inbound webhook. One per agent, or one per task if you want isolation. We cover the reasoning in more depth in why agents need a real inbox.
Step 1: create the mailbox
Fastest from the terminal:
npx @robotomail/cli mailbox create shopping-agent
Or over HTTP:
curl -X POST https://api.robotomail.com/v1/mailboxes \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"address": "shopping-agent"}'
The response contains the mailbox id and the full address. Store both with the agent's state. The id is what you use for sends; the address is what you put in prompts and signatures so the agent knows who it is.
Do this at provisioning time, not on every run. Creating a mailbox per invocation gives you a graveyard of addresses with no reply routing.
Step 2: send the message
How to send email from AI agent code in Python
import os
import requests
API_KEY = os.environ["ROBOTOMAIL_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def send_email(mailbox_id: str, to: list[str], subject: str, body: str,
in_reply_to: str | None = None) -> dict:
payload = {"to": to, "subject": subject, "bodyText": body}
if in_reply_to:
payload["inReplyTo"] = in_reply_to
response = requests.post(
f"https://api.robotomail.com/v1/mailboxes/{mailbox_id}/messages",
headers=HEADERS,
json=payload,
timeout=15,
)
response.raise_for_status()
return response.json()
send_email(
"mbx_shopping",
["vendor@example.com"],
"Order #A-4521 delivery update?",
"Hi, checking on the status of order A-4521.",
)
Wrap that as a tool and hand it to your framework of choice. Keep the tool signature small: to, subject, body, optional in_reply_to. Every extra parameter is another thing the model can get wrong.
The same thing in TypeScript
type SendArgs = {
mailboxId: string;
to: string[];
subject: string;
bodyText: string;
inReplyTo?: string;
};
export async function sendEmail(args: SendArgs) {
const { mailboxId, ...body } = args;
const res = await fetch(
`https://api.robotomail.com/v1/mailboxes/${mailboxId}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ROBOTOMAIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
},
);
if (!res.ok) {
throw new Error(`Send failed: ${res.status} ${await res.text()}`);
}
return res.json();
}
Note the explicit error branch. fetch does not throw on a 4xx, and an agent that treats a 422 as success will happily report "email sent" to the user. Surface the status code back into the agent's context so it can retry or escalate.
Step 3: handle the reply
Register a webhook and inbound mail arrives as JSON:
{
"event": "message.received",
"data": {
"message_id": "...", "mailbox_id": "...", "mailbox_address": "shopping-agent@robotomail.co",
"from": "vendor@example.com", "subject": "Re: ...", "body_text": "...",
"thread_id": "...", "received_at": "2026-04-17T10:00:00.000Z"
}
}
Two fields do most of the work. thread_id tells you which conversation this belongs to, so you can load prior turns instead of re-deriving context from a quoted signature block. message_id goes into inReplyTo when you reply, which keeps the thread intact in the recipient's client.
Note the field casing: outbound request bodies are camelCase, inbound webhook data is snake_case. Setup details are in the receive and reply guide and the threading concepts page.
Deliverability basics agents get wrong
The send works on the first try. Deliverability is where the two-week debugging sessions come from.
Sending from a platform domain forever. A shared sending domain is fine for testing and internal notifications. For anything a customer or vendor will read, move to your own domain and publish SPF, DKIM, and DMARC. Recipients judge the domain, and DMARC alignment is now effectively table stakes at the large mailbox providers. See how to configure DKIM and publish a DMARC record.
Letting the model invent recipients. LLMs hallucinate email addresses with real confidence. Validate syntax, and prefer resolving recipients from your own data rather than from model output. If the agent must supply an address freely, gate the first send to a new domain behind a check.
Ignoring bounces. A hard bounce means the address does not exist. Retrying it, or continuing to mail it in a later run, damages your reputation. Read bounce events and honour the suppression list. The difference between retry-worthy and permanent failures is covered in soft bounce vs hard bounce.
No rate limit at the agent layer. A loop bug that would be harmless in a scraper is a spam incident in email. Cap sends per mailbox per hour in your own code, on top of whatever the platform limits are. Also dedupe: if the agent has already emailed this address about this thread in the last N minutes, do not send again.
HTML for no reason. Agent email is mostly conversational. Plain text renders everywhere, avoids image-blocking issues, and reads like a person wrote it. Send bodyText unless you have a specific layout requirement.
Vague sender identity. The recipient should be able to tell within one line that they are talking to an automated system on behalf of a named company, and how to reach a human. This is both an ethics point and a spam-complaint point. More patterns in email automation best practices.
Testing against your own inbox only. Your Gmail account already trusts you. Test against a fresh address at a provider you do not control before you consider the pipeline working. See how to test email delivery.
Choosing where to send from
| Approach | Good for | Trouble |
|---|---|---|
| SMTP via a mail host | Legacy systems, one-off scripts | Blocking sockets, no inbound routing, credentials per mailbox |
| Bulk sending API | Marketing, one-way transactional mail | Receiving is bolted on or absent |
| Gmail or Outlook API | Agents acting inside one human's account | OAuth scopes, per-user consent, quotas designed for humans |
| Mailbox API | Agents that hold conversations | You do need to think about domains and DNS |
If you are weighing a specific one of these, we have direct comparisons for SendGrid and the Gmail API.
FAQ
Do I need SMTP to send email from an agent?
No. An HTTPS POST is easier to run inside an agent loop: it returns a message id you can log, it does not hold a socket open, and it works in serverless environments where outbound port 25 or 587 is blocked.
Can multiple agents share one mailbox?
They can, but separate mailboxes are usually better. One address per agent means inbound webhooks route unambiguously, rate limits are isolated, and revoking one agent does not touch the others. Details in mailbox concepts.
How do I keep replies in the same thread?
Pass the inbound message_id as inReplyTo on your reply, and keep the subject line consistent. The API sets the standard reply headers so mail clients group the messages.
What should the agent do when a send fails?
Distinguish transient from permanent. 5xx and network timeouts are worth a bounded retry with backoff. 4xx validation errors and hard bounces should stop the loop and surface to a human, not retry. Codes are listed in the errors reference.
Ready to wire it up? Create a mailbox, send your first message in a few minutes with the quickstart, or start at robotomail.com.
Give your AI agent a real email address
One API call creates a mailbox with full send and receive. Webhooks for inbound, automatic threading, deliverability handled. 30-day money-back guarantee.
Related posts

AI Email Agent: Build One That Sends and Receives
What an AI email agent is, and how to build one with its own mailbox, inbound webhook, reply loop, and correct threading. With working code.
Read post
Best Email APIs With Webhook Events for AI Agents
A webhook-first comparison of email APIs: inbound events, payload shape, signing, retries, ordering and replay for agents that react to incoming mail.
Read post
Email MCP Server: Give Any MCP Agent an Inbox
How to build an email MCP server: the tool surface for sending, replying, threading and receiving mail, with working TypeScript and API examples.
Read post