# How to Send Email From AI Agent: Python and TypeScript

Published: August 19, 2026

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

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](/blog/best-email-api-for-ai-agents-2026).

## 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](/blog/why-agents-need-real-inbox).

## Step 1: create the mailbox

Fastest from the terminal:

```bash
npx @robotomail/cli mailbox create shopping-agent
```

Or over HTTP:

```bash
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

```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

```ts
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:

```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](/docs/guides/receive-and-reply) and the [threading concepts page](/docs/concepts/threading).

## 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](/blog/how-to-configure-dkim) and [publish a DMARC record](/blog/publish-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](/blog/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](/docs/concepts/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](/blog/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](/blog/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](/compare/sendgrid) and the [Gmail API](/compare/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](/docs/concepts/mailboxes).

### 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](/docs/api/errors).

Ready to wire it up? Create a mailbox, send your first message in a few minutes with the [quickstart](/docs/quickstart), or start at [robotomail.com](https://robotomail.com).
