How to Send Email From AI Agent: Python and TypeScript

7 min read

Send email from AI agent code in Python or TypeScript: create a mailbox, one POST, plus the deliverability basics agents get wrong.

John Joubert

John Joubert

Founder, Robotomail

How to Send Email From AI Agent: Python and TypeScript
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