Developers
.mdAgent inboxes
Updated 16 Oct 2026
An agent inbox is an inbox of its own for an AI agent, on your verified domain, next to your team’s mailboxes. The agent reads and sends through the API or MCP, and people see its mail in the app like any other inbox. Every agent has a sponsor, a named person who answers for it, and the guardrails are on from the start.
- A person approves new recipients. Replies, and email to people the agent already corresponds with, go out right away. An email to anyone new waits for a person.
- Spoofed mail is held back. Mail whose sender fails authentication doesn’t reach the agent until a person releases it.
- It reads what a person would see. Hidden text is removed from every message, and each one says how far its sender can be trusted.
- It says it’s an AI. Every email it sends ends with a line saying an AI agent sent it, and for whom.
- It pauses on its own. Complaints or bounces on its mail pause that agent alone, long before they could add up to a pause for your whole workspace.
Create one
Agent inboxes live on a verified domain, so set up your domain first. Creating one is for workspace owners and admins.
In the app
Open Settings → Agents and create the agent: its name (it signs its email with it), its address, its sponsor, how freely it may send and what it may read. You can create its key in the same step. The key is shown once.
With the API
Use an owner’s or admin’s API key with the Manage mail scope. Only name is required; everything else has a safe default.
/api/v1/inboxesManage mailcurl -X POST "https://supermailos.com/api/v1/inboxes" \
-H "Authorization: Bearer $SUPERMAILOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Billing assistant","localPart":"billing","sendMode":"known","inboundMode":"verified","createKey":{"scopes":["read","write","send"],"expiresInDays":90}}'TypeScript
const res = await fetch("https://supermailos.com/api/v1/inboxes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUPERMAILOS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Billing assistant",
"localPart": "billing",
"sendMode": "known",
"inboundMode": "verified",
"createKey": {
"scopes": [
"read",
"write",
"send"
],
"expiresInDays": 90
}
}),
});
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
const data = await res.json();Python
import os
import requests
res = requests.post(
"https://supermailos.com/api/v1/inboxes",
headers={
"Authorization": f"Bearer {os.environ['SUPERMAILOS_API_KEY']}",
},
json={
"name": "Billing assistant",
"localPart": "billing",
"sendMode": "known",
"inboundMode": "verified",
"createKey": {
"scopes": ["read", "write", "send"],
"expiresInDays": 90,
},
},
timeout=30,
)
res.raise_for_status()
data = res.json()Request body (JSON)
| Name | Type | Description |
|---|---|---|
| namerequired | string | What the agent is called, such as Billing assistant. It signs its email with this name. |
| localPart | string | The part of its address before the @. Made from the name when you leave it out. |
| domain | string | One of your verified domains, by name or id. Your first verified domain when you leave it out. |
| sponsorUserId | string | The person who answers for the agent: approvals and alerts go to them. When you leave it out, the person the API key belongs to. |
| sendMode | approval | known | open | How freely it sends (see sending rules). Default known. |
| inboundMode | all | verified | allowlist | What it may read (see reading rules). Default all. |
| dailySendCap | integer | Emails it may send in 24 hours before the rest wait for a person. Default 100, at most 5000. |
| newRecipientsDailyCap | integer | In open mode, new recipients a day before the rest wait for a person. Default 20. |
| allowRecipients | string[] | Addresses or @domains it may write to without approval. |
| allowSenders | string[] | Addresses or @domains it may read in allowlist mode. |
| blockSenders | string[] | Addresses or @domains whose mail is never shown to it. |
| disclosure | boolean | End every email it sends with a line saying an AI agent sent it, and for whom. Default true; only a workspace admin can change it. |
| description | string | What the agent does, for the people who review it. |
| createKey | boolean or object | true, or { name?, scopes?, expiresInDays? }, to create its agent key in the same call. The answer then carries the key, shown once. |
It answers 201 with the new inbox (an AgentInbox) and, if you asked for one, its key. The key’s secret appears in this answer only, so store it now.
The inbox’s id is also its mailbox id. Pass it as mailbox to any mail endpoint or MCP tool, and the agent’s mail works like any other mailbox’s: list it, search it, reply from it.
Every agent endpoint
Each one links to its full entry, with parameters and samples in curl, TypeScript and Python, in the API reference.
| Operation | Endpoint | Scope |
|---|---|---|
| List agent inboxes | GET /inboxes | read |
| Create an agent inbox | POST /inboxes | write |
| Get an agent inbox | GET /inboxes/{id} | read |
| Update or pause an agent inbox | PATCH /inboxes/{id} | write |
| Delete an agent inbox | DELETE /inboxes/{id} | write |
| List an agent's keys | GET /inboxes/{id}/keys | read |
| Create an agent key | POST /inboxes/{id}/keys | write |
| Revoke an agent key | DELETE /inboxes/{id}/keys/{keyId} | write |
| List mail held back from agents | GET /holds | read |
| Release held mail to the agent | POST /holds/{id}/release | write |
| Keep held mail from the agent | POST /holds/{id}/dismiss | write |
| List agent emails waiting for a person | GET /approvals | read |
| Get an approval | GET /approvals/{id} | read |
| Approve and send an agent's email | POST /approvals/{id}/approve | send |
| Reject an agent's email | POST /approvals/{id}/reject | write |
Agent keys
An agent key reaches one agent inbox and nothing else: not your team’s mailboxes, not other agents. Give each agent its own key, and keep your own keys to yourself.
/api/v1/inboxes/{id}/keysManage mailcurl -X POST "https://supermailos.com/api/v1/inboxes/INBOX_ID/keys" \
-H "Authorization: Bearer $SUPERMAILOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Production worker","scopes":["read","send"],"expiresInDays":30}'- Create one with this endpoint, or with
createKeywhen you create the inbox. Owners and admins only. - Scopes are the usual three: read, write and send, all three unless you choose. Leave out send and the agent can read and draft, but never send.
- Expiry is optional:
expiresInDays, from 1 to 3650. Without it, the key works until you revoke it. - It is shown once. Revoke it with
DELETE /api/v1/inboxes/{id}/keys/{keyId}. Deleting the inbox deletes its keys.
An agent key can’t approve or reject email, release held mail, change its own rules, or create agents and keys. Those are for people, and the MCP server hides those tools from an agent’s key.
Agent inboxes have no IMAP or SMTP app passwords. An agent works through the API or MCP, which is where its rules are enforced. To connect it to an MCP client, use its key as the bearer token for https://supermailos.com/mcp; the setup for each client is in Connect your AI.
Sending rules and approvals
The rules belong to the inbox, so they apply whichever API key sends from it. sendMode decides how freely it sends:
| sendMode | What happens |
|---|---|
| known (default) | Replies, and email to people the agent already corresponds with, go out right away. An email to a new recipient waits for a person. |
| approval | Every email waits for a person. |
| open | It sends on its own, up to 20 new recipients a day unless you change that. Past the limit, emails wait for a person. |
Two more settings apply in every mode:
dailySendCap: at most this many emails in 24 hours (default 100). Past it, emails wait for a person.allowRecipients: addresses or whole domains (@example.com) the agent may always write to without approval.
People the agent already corresponds with are everyone who wrote to it and proved who they are (their email passed DMARC), everyone it has written to, the recipients of any email a person approved, and the senders of mail a person released to it. Anyone can write any address in an email’s From, so an unproven sender never counts.
When an email waits
A send that has to wait still answers 202, with status: "pending_approval". Nothing was sent yet. An email that did go out also answers 202, with status: "sent", so check status, not the HTTP code.
{
"ok": true,
"status": "pending_approval",
"approvalId": "apr_4f1c2b7e9a",
"reasons": ["new recipient client@example.com"],
"expiresAt": "2026-10-23T09:30:00.000Z",
"mailboxId": "mbx_6e2a91c0d4",
"from": "billing@acme.com"
}Response · PendingSendResponse
| Name | Type | Description |
|---|---|---|
| okrequired | boolean | true: the request was accepted. |
| statusrequired | string | pending_approval: nothing was sent yet. |
| approvalIdrequired | string | GET /approvals/{approvalId} for what became of it. |
| reasonsrequired | string[] | Why it waits, in plain words. |
| expiresAtrequired | string (date-time) | When it expires unless a person decides: 7 days after the agent asked. |
| mailboxIdrequired | string | The agent inbox it would go out from. |
| fromrequired | string (email) | The address it would go out from. |
The agent’s sponsor is alerted by email. Then a person who can send from the agent’s inbox decides, in the app or with the API. The app shows the email exactly as it would go out: its HTML as recipients see it, its plain-text version and the disclosure line.
- Approve sends it as written, as the agent, through every usual check. It needs the Send mail scope.
- Reject sends nothing. An optional
notetells the agent why. It needs the Manage mail scope.
curl -X POST "https://supermailos.com/api/v1/approvals/APPROVAL_ID/approve" \
-H "Authorization: Bearer $SUPERMAILOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note":"Looks right."}'curl -X POST "https://supermailos.com/api/v1/approvals/APPROVAL_ID/reject" \
-H "Authorization: Bearer $SUPERMAILOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note":"Don'\''t quote a price; ask them to call."}'The agent finds out by polling GET /api/v1/approvals/{id}, or from the agent.approval_decided webhook. Its own key can never approve its own email. Each approval is in one of these states:
| status | Meaning |
|---|---|
| pending | Waiting for a person. |
| approved | A person approved it and it is being sent. |
| sent | Approved and sent as written. result has its messageId and threadId. |
| rejected | A person rejected it and nothing was sent. decisionNote carries their note, if they left one. |
| failed | Approved, but it couldn’t be sent. result says why. |
| expired | Nobody decided within 7 days. Nothing was sent. |
Retry a send with the same key and the same Idempotency-Key and you get the same approval back, so an email never waits twice. A different email under a key already used answers 422 idempotency_key_reused. Up to 200 emails can wait per agent; past that, its sends answer 429 until people catch up.
Reading rules and held mail
SuperMailOS checks SPF, DKIM and DMARC on every message to an agent inbox itself, then decides whether the agent may read it. inboundMode sets how strict that is:
| inboundMode | What the agent reads |
|---|---|
| all (default) | Everything, except messages that fail authentication and mail from blocked senders. |
| verified | Only mail whose sender passed authentication. |
| allowlist | Only mail from senders on its allowlist, and only when they passed authentication. |
Failing authentication is specific: the sender’s domain tells receivers to reject or quarantine mail that fails its checks, and this message failed them. Mail from a domain without such a policy counts as unverified, not failed. In every mode, failing mail and mail from blockSenders stay away from the agent.
Mail the agent may not read is held: its conversation doesn’t exist for the agent’s key until a person releases it. People still see it in their inbox as usual, and the agent’s sponsor is alerted by email. Each hold says why, and a conversation’s hold shows the worst reason among its messages: a spoof that arrives in a conversation held for an unverified sender turns it into a spoof’s hold.
| reason | Why it was held |
|---|---|
| auth_failed | The sender failed authentication (DMARC): it may not be who it claims to be. |
| blocked_sender | The sender is on this agent's block list. |
| not_allowlisted | The sender isn't on this agent's allowlist. |
| unverified_sender | The sender couldn't be verified, and this agent only reads verified mail. |
A person who can manage the agent’s mail decides, in the app or with the API:
GET /api/v1/holdslists what is held. Add?inbox=for one agent.POST /api/v1/holds/{id}/releaselets the agent read the conversation from now on, firesmessage.inboundfor it, and counts the sender as someone the agent corresponds with. Send thereasonyou reviewed ({ "reason": "unverified_sender" }): if a worse email has arrived in the conversation since, nothing is released and you get409.POST /api/v1/holds/{id}/dismisskeeps it from the agent.
curl "https://supermailos.com/api/v1/holds?inbox=INBOX_ID&status=held" \
-H "Authorization: Bearer $SUPERMAILOS_API_KEY"Release only mail you have looked at: a released message reaches the agent as it is. Until then, message.inbound doesn’t fire for it; the message.held webhook carries ids and the sender’s address only, never the subject or a snippet.
What an agent reads
An agent’s key reads the visible text of a message, nothing more. Before it gets a message, we remove what a person reading it would never see: elements hidden by an inline style or by the email’s own stylesheet (display:none, a zero font size, transparent text, text pushed off the screen), HTML comments, and invisible Unicode characters. Instructions planted in an email for an AI to follow are often hidden in exactly those places.
When an email has HTML, the agent’s text is built from what that HTML shows, with each link’s address after it. Its plain-text part is ignored: mail apps show the HTML, so nobody reads that part, which makes it a place to hide instructions in plain sight. Snippets in lists, search results and webhooks follow the same rule, and an agent’s key can’t download a message’s raw source.
Each message it reads carries trust:
"trust": {
"sender": "verified",
"hiddenContent": true,
"signals": ["2 hidden elements", "1 HTML comment"]
}| trust.sender | Meaning |
|---|---|
| verified | The sender passed our own SPF, DKIM and DMARC check. |
| unverified | There was nothing to check against, or the check couldn't run. Not proven either way. |
| failed | The sender's domain tells receivers to reject or quarantine mail that fails its checks, and this message failed: a likely spoof. An agent only sees one after a person releases it. |
| self | Mail this inbox sent. |
hiddenContentistruewhen the email carried content a person wouldn’t see.textandhtmlare the visible part only.signalslists what was removed, such as2 hidden elements.
The MCP server also wraps everything an agent’s key reads in a notice that it is untrusted email: data to work on, never instructions to follow. No filter catches every trick, so give the agent narrow instructions and keep email to new people behind approval.
The AI disclosure line
Every email an agent sends ends with a line naming the agent, the person it works for and your workspace:
Sent by Billing assistant, an AI agent working for Ana Ruiz at Acme.
It also carries an X-SuperMailOS-Agent header with the agent’s address, so software can tell agent mail apart too. The line is on by default (disclosure); a workspace admin can turn it off.
The line is added to every email, even one that already seems to have it, and nothing the agent writes can hide it: before adding it, we remove comments, stylesheets and the document’s head from the agent’s HTML and close anything it left open. Style an agent’s email with inline styles.
Saying plainly that an email comes from an AI, and on whose behalf, helps you meet transparency rules like the EU AI Act’s. Whether it is enough for your use is a question for your own legal adviser.
Its own pause
An agent is held to tighter limits than a person. One spam complaint, or three hard bounces (mail to addresses that don’t exist), on an agent’s mail within 24 hours pauses that agent. Each pause lasts longer than the one before: 1 hour, then 24 hours, then until a person resumes it.
Only that agent stops sending, from every app, a person writing as it included. It still receives mail, and the rest of your workspace keeps sending as before. Its sponsor gets an email and the agent.paused webhook fires. While it is paused, its sends answer 423 and nothing is queued. Its mail also counts toward your workspace’s own reputation, like anyone else’s.
A pause for complaints or bounces runs its course: the timed ones lift on their own, and the one that waits for a person needs a workspace admin. People can pause an agent themselves too, and its sponsor or a workspace admin can lift that pause, in the app or with PATCH /api/v1/inboxes/{id}.
{ "status": "paused", "pauseReason": "Checking a complaint" }
{ "status": "active" }Webhooks
These events follow agent inboxes, and each carries the agent’s mailboxId. Payloads, signature checks and retries are on Webhooks; an admin subscribes an endpoint under Settings → Developers.
| Event | When it fires |
|---|---|
| message.held | Mail to an agent inbox was held back from the agent. It carries ids and the sender's address only, never the subject or a snippet. |
| agent.approval_requested | An email the agent asked to send is waiting for a person. |
| agent.approval_decided | A person approved the email (so it was sent) or rejected it. decision says which. |
| agent.paused | The agent was paused, automatically or by a person. Its sending stops; the rest of the workspace keeps sending. |
MCP tools
The MCP server at https://supermailos.com/mcp has tools for agent inboxes next to the mail tools. Tools for people are hidden from an agent’s own key, which works its inbox with the ordinary mail tools (list_threads, get_thread, reply_to_thread, send_email) under the rules above.
| Tool | What it does | Scope | For |
|---|---|---|---|
list_agent_inboxes | The workspace's AI agent inboxes: address, sponsor, pause state, send and read rules, and emails waiting for approval. | read | People and agents |
create_agent_inbox | Creates an inbox of its own for an AI agent on your domain, with safe defaults. For people, not agents. | write | People |
update_agent_inbox | Changes an AI agent's send and read rules, or pauses and resumes its sending. For people, not agents. | write | People |
list_agent_approvals | Emails AI agents asked to send that wait for a person, with why they wait. An agent sees only its own outcomes. | read | People and agents |
decide_agent_approval | Approves (sends) or rejects an email an AI agent asked to send. For people, not agents. | send | People |
list_held_mail | Mail held back from AI agents because its sender failed authentication or isn't allowed. For people, not agents. | read | People |
decide_held_mail | Releases held mail to the AI agent, or keeps it from the agent. For people, not agents. | write | People |
Creating an agent over MCP doesn’t create its key: that happens in the app or with the API. How to connect each client is in Connect your AI.
Limits by plan
Agent inboxes never count as paid seats. Each paid seat includes some, and the trial includes a few:
| Plan | Agent inboxes included |
|---|---|
| Pro | 2 with each paid seat |
| Business | 5 with each paid seat |
| 14-day trial | 3 in total |
So a Business workspace with 4 paid seats can have 20 agent inboxes. Need more? Add a seat. At the limit, creating another answers 403 with the code limit_reached. Plans and seats are on the pricing page.
Objects
The shapes the agent endpoints return. Fields marked required are always present; the rest appear when they apply.
AgentInbox
An agent inbox and its settings. Its id is its mailbox id.
| Name | Type | Description |
|---|---|---|
| idrequired | string | Also its mailbox id: pass it as mailbox to the mail endpoints. |
| addressrequired | string (email) | Its email address. |
| namerequired | string | What the agent is called; it signs its mail with it. |
| description | string | What the agent does, for the people who review it. |
| statusrequired | active | paused | paused while a pause is in force, otherwise active. |
| paused | object | While it is paused: reason, by, since, and until (absent when it waits for a person to resume it). |
| sponsorrequired | object | The person who answers for the agent: approvals and alerts go to them. |
| sendModerequired | approval | known | open | How freely it sends. |
| inboundModerequired | all | verified | allowlist | What it may read. |
| dailySendCaprequired | integer | Emails it may send in 24 hours before the rest wait for a person. |
| newRecipientsDailyCaprequired | integer | In open mode, new recipients a day before the rest wait for a person. |
| allowSendersrequired | string[] | Addresses or @domains the agent may read in allowlist mode. |
| blockSendersrequired | string[] | Never shown to the agent. |
| allowRecipientsrequired | string[] | The agent may write to these without approval. |
| disclosurerequired | boolean | Its mail ends with a line saying an AI agent sent it, for whom. |
| createdVia | app | api | mcp | Where it was created. |
| createdAtrequired | string (date-time) | When it was created. |
| updatedAt | string (date-time) | When its settings last changed. |
| pendingApprovals | integer | Emails waiting for a person (people's keys only). |
AgentKey
A key that reaches one agent inbox. The secret appears only in the answer that created it.
| Name | Type | Description |
|---|---|---|
| idrequired | string | The key's id, for revoking it. |
| namerequired | string | Its label. When you don't name it, Agent: followed by the agent's name. |
| prefixrequired | string | Non-secret leading characters. |
| scopesrequired | string[] | Any of read, write and send. |
| inboxIdrequired | string | The one agent inbox it reaches. |
| createdAtrequired | string (date-time) | When it was created. |
| lastUsedAt | string (date-time) | When it was last used. |
| expiresAt | string (date-time) | When it stops working, if it expires. |
| revokedAt | string (date-time) | When it was revoked. |
| secret | string | Only when it was just created: the key itself (smk_…). Shown once. |
AgentApproval
An email an agent asked to send that waits, or waited, for a person. An agent's own key never gets the body or the Bcc line back.
| Name | Type | Description |
|---|---|---|
| idrequired | string | The approval's id, to poll, approve or reject it. |
| inboxIdrequired | string | The agent inbox that asked. |
| kindrequired | send | reply | send for a new email, reply for a reply in a conversation. |
| threadId | string | The conversation, for a reply. |
| fromrequired | string (email) | The address it goes out from. |
| torequired | string | Recipients, as the agent wrote them. |
| ccrequired | string | Cc recipients, as the agent wrote them. |
| bcc | string | People's keys only. |
| subjectrequired | string | The subject the agent wrote. |
| text | string | The body the agent wrote (people's keys only). |
| html | string | Its HTML version, when it wrote one (people's keys only): what most mail apps show. |
| reasonsrequired | string[] | Why it waits, in plain words. |
| statusrequired | pending | approved | rejected | sent | failed | expired | Where it stands. See the states under sending rules. |
| createdAtrequired | string (date-time) | When the agent asked. |
| expiresAtrequired | string (date-time) | When it expires unless a person decides. |
| decidedAt | string (date-time) | When a person decided. |
| decisionNote | string | What the person said when deciding. |
| result | object | Once sent: { messageId, threadId, mailboxId }. Once failed: { error, code }. |
AgentHold
Mail held back from an agent, and what a person decided.
| Name | Type | Description |
|---|---|---|
| idrequired | string | The hold's id, to release or dismiss it. |
| inboxIdrequired | string | The agent inbox it was held from. |
| threadIdrequired | string | Hidden from the agent's key while held or dismissed. |
| emailId | string | The held message. |
| senderrequired | string (email) | The From address, and only the address. |
| reasonrequired | auth_failed | blocked_sender | not_allowlisted | unverified_sender | Why it was held. |
| reasonTextrequired | string | The reason in plain words. |
| verdictrequired | verified | unverified | failed | Our own authentication verdict for the sender. |
| auth | object | The SPF, DKIM and DMARC results. |
| statusrequired | held | released | dismissed | Held, released to the agent, or kept from it. |
| createdAtrequired | string (date-time) | When it was held. |
| decidedAt | string (date-time) | When a person released or dismissed it. |
Next: the API reference for every agent endpoint, or webhooks to hear about approvals, holds and pauses as they happen.
Was this page helpful?