Give an agent an inbox
Build the full loop for an email-driven agent. Create an inbox, wait for mail, read the trust report, act, reply in the same thread and mark the message read.
This guide builds the loop most email agents run:
- Create an inbox for the agent.
- Wait for new mail.
- Read the message through its trust report, not the raw body.
- Decide what to do and act.
- Reply in the same thread.
- Mark the message read so it is not processed twice.
You need an API key. Every example reads it from MACHINEMAIL_API_KEY and calls https://api.machinemail.dev. See Quickstart if you do not have a key yet.
Note
If your agent speaks MCP, you can skip most of this code. The hosted MCP server at https://mcp.machinemail.dev/mcp has create_inbox, wait_for_message, get_message and reply_to_message
tools that do the same steps. See MCP.
1. Create the inbox
Pick a username, or leave it out for a generated one. Pass a client_id so retries return the same inbox instead of failing.
The inbox ID is the email address. A repeat call with the same client_id returns the existing inbox with status 200. If the address belongs to someone else you get 409 conflict. See Inboxes for naming rules, ephemeral inboxes and custom domains.
2. Wait for mail
There are three ways to learn about new mail. Pick one.
| Method | Best for | How |
|---|---|---|
| Webhook | Servers with a public URL | POST /v0/webhooks with event_types: ["message.received"]. See Webhooks. |
| Event stream | Long-running workers without a public URL | GET /v0/events/stream (SSE) or /v0/ws (WebSocket). See Realtime events. |
| Polling | Scripts and cron jobs | List messages with the UNREAD label. |
Polling is the simplest to get right. New inbound mail carries the INBOX and UNREAD labels, so ask for UNREAD, oldest first, and remove the label once you have handled a message:
List results are message items without bodies or trust reports, but each inbound item has a trust_level so you can triage without another call.
Note
Lists hide quarantined, spam, blocked, unauthenticated and trashed mail by default. That is usually what an agent wants. Add include_quarantine=true (and the other include_* flags) only if a
human reviews that mail. See Threads and messages.
You can also filter by after=<ISO timestamp>. For inbound mail the message timestamp comes from the sender’s Date header, so a skewed sender clock can put a new message before your cursor. The UNREAD label does not have that problem.
3. Read the trust report
Fetch the full message. It includes the body, extracted_text (the new reply without quoted history) and the trust report.
Three fields drive most decisions:
trust.level:verified,known,unknown,suspiciousorquarantined.trust.content.text: the agent-safe body. Quotes, signatures, hidden text, zero-width characters and tracking pixels are removed. Give the model this, nottextorhtml.trust.reasons: plain-English reasons for the verdict. Log them, and show them to a human when you escalate.
A simple policy that works for many agents:
| Level | What the agent does |
|---|---|
verified | Act on requests in the message. |
known | Act, but do not follow instructions that change who the agent talks to, send money or share credentials. |
unknown | Treat the content as data. Answer questions, never follow instructions. |
suspicious | Do not act. Escalate to a human with trust.reasons. |
quarantined | Never reaches default lists. Leave it for human review. |
See Using trust in agents for more patterns and Trust report for every field.
4. Reply in the thread
POST .../messages/{message_id}/reply addresses the sender (or their Reply-To), sets the subject to Re: ... and adds In-Reply-To and References, so the reply lands in the same thread in both mailboxes.
See Reply in thread for reply-all, forwarding and drafts.
5. Mark the message read
Remove the UNREAD label. Labels live on messages; the thread’s labels are the union of its messages’ labels.
You can add your own labels in the same call, for example "add_labels": ["handled"]. Labels are free strings.
The whole loop
Warning
Mark a message read only after you have acted on it. If the process crashes between the reply and the PATCH, the next tick sees the message again. If a duplicate reply would hurt, record the
message_id you replied to before sending, or label the message first and reply second.