Skip to content

An email inbox for your AI agent

An agent that signs up for things needs an address that receives mail, and a way to get the code out of it without polling. Here is the whole flow in three steps.

Updated 3 min read

On this page (7)

Why agents need their own inbox

A browser agent that signs up for a service hits the same wall every time: “we sent you a code”. Using your personal inbox means giving the agent your mail, and parsing a shared inbox means guessing which message belongs to which run. An inbox per agent, or an address per run, keeps the agent's mail apart from yours and makes the right message easy to find.

Mailbay is built for this. Every workspace gets a private domain like k7m3qz.mailbay.email that works before you touch DNS, and one call waits for the email and hands back the code.

The flow

  1. Create an inbox (or make up an address on the Catch-all).
  2. The agent types that address into the sign up form.
  3. Call wait. It returns when the email arrives, with codes and links already pulled out.

1. Get an API key

Sign in to the dashboard and create a key on the API keys page. Keys look like mb_live_… and are shown once. Put it in your environment:

export MAILBAY_API_KEY=mb_live_xxxxxxxxx

2. Create an inbox

curl https://api.mailbay.dev/v1/inboxes \
  -H "Authorization: Bearer $MAILBAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"local_part": "agent-signup"}'
response
{
  "id": "ibx_01m4x446p1x736eqmjgh4k7f34",
  "address": "agent-signup@s8k6ed.mailbay.email",
  "local_part": "agent-signup",
  "domain": "s8k6ed.mailbay.email",
  "catch_all": false,
  "created_at": "2026-10-14T11:43:49.734211Z"
}

The address receives mail immediately. Leave out local_part for a random one. To put the inbox on your own domain, add "domain": "acme.com" after verifying it (see Domains).

3. Wait for the email

After the agent submits the form, one request blocks until a message arrives, up to 120 seconds. Filter with from or subject (each matches part of the value) if other mail might land first.

curl "https://api.mailbay.dev/v1/inboxes/ibx_01m4x446p1x736eqmjgh4k7f34/wait?from=example.com&timeout=120" \
  -H "Authorization: Bearer $MAILBAY_API_KEY"
response (message shortened)
{
  "message": {
    "id": "msg_…",
    "from": "no-reply@example.com",
    "subject": "Your verification code",
    "text": "Your code is 482913. Or confirm with https://example.com/verify?token=abc123",
    "spam": false,
    "auth_results": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam_score": 0.4 }
  },
  "codes": ["482913"],
  "links": ["https://example.com/verify?token=abc123"]
}

codes holds standalone 4 to 8 digit numbers in order of appearance, without years from footers. links holds the https links in the message. If nothing arrives in time the answer is wait_timeout, and calling again is safe.

wait only matches mail received after the call starts. If the email might arrive before you start waiting, pass after with the inbox's created_at, or a time you noted before submitting the form.

The whole thing in Python, with only the standard library:

signup_agent.py
import json, os, urllib.parse, urllib.request

API = "https://api.mailbay.dev"
KEY = os.environ["MAILBAY_API_KEY"]

def call(method, path, body=None):
    req = urllib.request.Request(API + path, method=method,
        data=json.dumps(body).encode() if body else None,
        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=130) as r:
        return json.load(r)

inbox = call("POST", "/v1/inboxes", {"local_part": "agent-signup"})
print("sign up with", inbox["address"])

# ... the agent submits inbox["address"] to the sign up form here ...

q = urllib.parse.urlencode({"after": inbox["created_at"], "timeout": 120})
mail = call("GET", f"/v1/inboxes/{inbox['id']}/wait?{q}")
print("code:", mail["codes"][0] if mail["codes"] else None)
print("link:", mail["links"][0] if mail["links"] else None)

One address per run

Agents that sign up often do not need an inbox each. Any address under your code that is not a named inbox lands in the Catch-all inbox, which Mailbay creates in your Playground project and which does not count against your inbox limit. Make up an address per run and wait for exactly that address with to:

ADDRESS="run-$(date +%s)@s8k6ed.mailbay.email"
# the agent signs up with $ADDRESS, then:
curl "https://api.mailbay.dev/v1/inboxes/$CATCH_ALL_ID/wait?to=$ADDRESS&timeout=120" \
  -H "Authorization: Bearer $MAILBAY_API_KEY"

Your code and full domain are in GET /v1/me as shared_domain, and the Catch-all appears in GET /v1/inboxes with "catch_all": true. The same filter works in the CLI: mailbay wait ibx_… --to run-42@s8k6ed.mailbay.email.

Limit what the agent can touch

Give the agent a key that reaches one inbox and nothing else. An inbox key can read, wait and reply in its inbox; listing other inboxes, creating inboxes or sending from a domain answers 403 insufficient_scope.

curl https://api.mailbay.dev/v1/keys \
  -H "Authorization: Bearer $MAILBAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "signup agent", "permissions": "inbox", "inbox_id": "ibx_01m4x446p1x736eqmjgh4k7f34"}'
  • Until your workspace has a verified domain, mail sent from mailbay.email addresses can only reach your own team and the test addresses, so an agent cannot email strangers from a fresh account.
  • Received mail carries SPF, DKIM and DMARC results and a spam flag, so an agent can ignore mail that fails them.
  • Prefer tools to code? The same flow is two MCP tool calls: see give an MCP agent its own email.

Join the beta

Mailbay is in private beta and free while the beta runs: inboxes, sending, your own domains and MCP. We invite people in small batches and read every note on the form.