Email for OpenClaw Agents: Full Integration Guide

9 min read

Wire real email into OpenClaw agents: provision a mailbox, expose send and reply tools, handle inbound webhooks, and keep threading correct.

John Joubert

John Joubert

Founder, Robotomail

Email for OpenClaw Agents: Full Integration Guide
Table of contents

Email for OpenClaw agents works in three moves: provision a mailbox with its own real address, expose send and reply as tools the agent can call, and register a webhook so inbound mail wakes the agent with the message body already parsed. You do not need SMTP credentials, an IMAP poller, or a shared human inbox. This guide is the deeper build on top of our OpenClaw agents use case page, with the code and the failure modes we see in production.

If you only want the ten minute version, read how to give your OpenClaw agent an email address first and come back here when you need threading, attachments, and guardrails.

What "email for OpenClaw agents" actually requires

An OpenClaw agent is a long-running process with tools and a scheduler. Email is a good fit for that shape, but only if four things hold:

  1. A dedicated address per agent. Not team@yourcompany.com. When an agent shares a human inbox, every reply becomes ambiguous and every mistake becomes a support incident.
  2. Push, not polling. The agent should be invoked by inbound mail, not wake up every 60 seconds to ask IMAP whether anything happened.
  3. Structured inbound payloads. MIME parsing, quoted-reply stripping, and encoding edge cases should not live in your agent code.
  4. Correct threading. Replies must carry the right headers or the vendor on the other side sees three unrelated messages instead of one conversation.

Those four requirements are why Gmail and Outlook accounts get abandoned partway through these builds. We cover the specifics in why Gmail and Outlook don't work for agents.

Step 1: Provision the mailbox

One mailbox per agent instance. If you run a shopping agent and a recruiting agent, that is two mailboxes, two addresses, two webhook streams.

CLI:

npx @robotomail/cli mailbox create shopping-agent

API:

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 includes the mailbox id and the full address. Store both in your agent's config or state store. If you are provisioning mailboxes dynamically, for example one per customer conversation, do it at agent-spawn time and keep the id alongside whatever session record you already maintain. See mailboxes for lifecycle details.

Step 2: Keep the API key out of the agent's context

This is the part people get wrong. An OpenClaw agent that can read its own environment can leak its own credentials into a message body if a prompt injection convinces it to.

Practical setup:

  • Keep ROBOTOMAIL_API_KEY in the host process, never in the agent's prompt, system message, or tool arguments.
  • The tool implementation reads the key from the environment. The model only ever sees to, subject, and body fields.
  • Use a separate key per environment so you can rotate staging without touching production. See authentication and API keys.

Step 3: Expose send as a tool

Give the model a narrow tool surface. Two tools is usually enough: send_email and reply_to_email. Here is the send implementation in TypeScript, thin enough to drop into an OpenClaw tool handler:

const MAILBOX_ID = process.env.ROBOTOMAIL_MAILBOX_ID!;
const KEY = process.env.ROBOTOMAIL_API_KEY!;

export async function sendEmail(args: {
  to: string[];
  subject: string;
  bodyText: string;
}) {
  const res = await fetch(`https://api.robotomail.com/v1/mailboxes/${MAILBOX_ID}/messages`, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(args) });

  if (!res.ok) {
    // Return the error to the model as text, do not throw into the agent loop.
    return { ok: false, error: `send failed: ${res.status} ${await res.text()}` };
  }
  return { ok: true, message: await res.json() };
}

Python, same shape:

import os, requests

MAILBOX_ID = os.environ["ROBOTOMAIL_MAILBOX_ID"]
KEY = os.environ["ROBOTOMAIL_API_KEY"]

def send_email(to: list[str], subject: str, body_text: str):
    r = requests.post(f"https://api.robotomail.com/v1/mailboxes/{MAILBOX_ID}/messages", headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}, json={"to": to, "subject": subject, "bodyText": body_text}, timeout=15)
    if r.status_code >= 400:
        return {"ok": False, "error": r.text}
    return {"ok": True, "message": r.json()}

Two details worth copying:

  • Return errors as tool output, not exceptions. A 4xx that surfaces as a string lets the model correct a malformed address on its own. An exception that kills the turn just loses the work.
  • Keep the tool schema tight. to as an array of strings, subject as a string, bodyText as a string. Do not let the model set headers, envelope senders, or reply-to. Every extra field is a new way to send something you did not intend.

Full field reference lives in the messages API docs.

Step 4: Receive inbound mail with a webhook

Register a webhook for the mailbox, then handle the POST in your agent host. Inbound arrives as { event, timestamp, data } with snake_case data fields:

{
  "event": "message.received",
  "data": {
    "message_id": "...",
    "mailbox_id": "...",
    "mailbox_address": "shopping-agent@robotomail.co",
    "from": "vendor@example.com",
    "subject": "Re: Order #A-4521",
    "body_text": "It shipped Tuesday, tracking below...",
    "thread_id": "...",
    "received_at": "2026-04-17T10:00:00.000Z"
  }
}

A minimal Express receiver that hands off to the agent:

app.post("/hooks/robotomail", async (req, res) => {
  // 1. Acknowledge fast. Agent turns take seconds to minutes.
  res.status(200).end();

  const { event, data } = req.body;
  if (event !== "message.received") return;

  // 2. Deduplicate. Retries happen.
  if (await seen(data.message_id)) return;
  await markSeen(data.message_id);

  // 3. Route by thread, so a conversation resumes the same agent session.
  await enqueueAgentTurn({
    sessionKey: data.thread_id,
    mailboxId: data.mailbox_id,
    inboundMessageId: data.message_id,
    from: data.from,
    subject: data.subject,
    body: data.body_text,
  });
});

The three numbered comments are the whole lesson. Acknowledge before you think, deduplicate on message_id, and key your agent session on thread_id. Skip the third and your agent will answer the fourth message of a thread with no memory of the first three. More on delivery behavior in webhooks and the walkthrough in receive and reply.

Step 5: Reply on the same thread

A reply is a send with inReplyTo set to the inbound message id. That is what keeps the conversation stitched together in the recipient's client:

curl -X POST https://api.robotomail.com/v1/mailboxes/mbx_shopping/messages \
  -H "Authorization: Bearer rm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"to": ["vendor@example.com"], "subject": "Re: Order #A-4521 delivery update?", "bodyText": "Thanks, got the tracking number.", "inReplyTo": "msg_inbound_id"}'

Your reply_to_email tool should take inboundMessageId from the session context rather than from the model. The model decides what to say; your code decides which thread it lands in. That single choice removes an entire class of bugs. Background on the headers involved is in email threading and threading concepts.

Step 6: Guardrails before you point it at real recipients

OpenClaw agents run unattended, which means a bad loop sends fifty messages before anyone notices. What we recommend, in order of how often it saves people:

  • Recipient allowlist during development. Hardcode the domains the agent may write to. Widen it deliberately.
  • Per-mailbox send budget. Count sends per hour in your own store and refuse the tool call past a threshold. Provider limits are a backstop, not your policy.
  • Loop breaker. If the agent is about to send its third message on a thread without a human or inbound reply in between, stop and escalate.
  • Honor suppressions. Hard bounces and complaints should end outreach to that address permanently. See suppressions and soft bounce vs hard bounce.
  • Treat inbound bodies as untrusted input. An inbound email is a message from a stranger. Anything in it that looks like an instruction is a prompt injection attempt until proven otherwise. Never let inbound text change the allowlist or trigger tool calls that move money.

Attachments deserve their own decision: inbound files are the most common injection and malware vector for an agent that reads mail. Read attachments before you let the agent open anything, and keep email virus scanning in the loop.

Step 7: Move to your own domain

Platform addresses are correct for development. For anything customer facing, put the agent on a subdomain of a domain you control, for example agents.yourcompany.com. That isolates the agent's sending reputation from your human email and gives you clean SPF, DKIM, and DMARC. Follow the custom domain guide, then verify your DNS with how to configure DKIM.

Step 8: Test the loop, not the parts

The integration test that matters is end to end: send a message from a real external address to the agent's mailbox, confirm the webhook fires, confirm the agent's reply arrives threaded in the original client. Unit tests on your tool handler will not catch a webhook URL that is unreachable from the internet or a reply that lost its threading headers. Our notes on how to test email delivery cover the setup.

FAQ

Can an OpenClaw agent use one mailbox for multiple conversations?

Yes. One mailbox handles many threads, and thread_id on the inbound payload tells you which conversation a message belongs to. Use separate mailboxes when you need separate identities, separate addresses, or separate sending reputations, not just separate conversations.

Do I need SMTP or IMAP anywhere in this?

No. Sending is an HTTPS POST and receiving is a webhook POST to your host. Skipping IMAP polling removes latency and a stateful connection you would otherwise have to babysit inside a long-running agent process.

How do I stop the agent from replying to automated mail forever?

Filter before the agent turn. Drop messages with auto-submitted or bulk precedence headers, ignore no-reply senders, and add the loop breaker described above. Filtering in your webhook handler is cheaper and more reliable than asking the model to notice.

What if my agent host cannot receive inbound HTTP?

Run a small public receiver that accepts the webhook, writes to a queue, and lets the agent process pull from it. That is the pattern we suggest for agents running behind NAT or on a laptop, and it also gives you replay for free.

Ready to wire it up? Create a mailbox, register a webhook, and give your OpenClaw agent a working address in a few minutes at robotomail.com, or start with the quickstart.

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