Developer docs

HermesAgentMail API

Agent inbox infrastructure over a simple REST API. Create inboxes on hermesagentmail.com, receive inbound email through signed webhooks, and send replies with human approval built in. Everything lives at https://hermesagentmail.com — no separate API subdomain.

Overview

HermesAgentMail gives every agent workflow its own address on hermesagentmail.com. Inbound mail is verified, queued, stored encrypted, and pushed to your endpoints. Outbound mail goes through an approval model so a human signs off before an agent sends, unless you explicitly opt an inbox into auto mode.

Inbound flow

  1. 1Email arrives at your-inbox@hermesagentmail.com.
  2. 2Resend receives the message and delivers a verified webhook to us.
  3. 3The event lands in a durable queue for reliable processing.
  4. 4A worker stores the message encrypted (AES-256-GCM at the application layer).
  5. 5An HMAC-signed webhook is POSTed to the endpoints you configured in the dashboard under Webhooks.

REST API

JSON over HTTPS at hermesagentmail.com/api/v1. Bearer token auth. Predictable errors.

Webhook-first

Inbound email is pushed to you, HMAC-signed and verifiable.

Approvals

Replies wait for human approval unless the inbox is set to auto.

Authentication

Authenticate every request with an API key in the Authorization header. Keys are created in the dashboard, look like hm_live_…, are stored as SHA-256 hashes at rest, and are scoped — a key can only call endpoints its scopes allow. Revoke a key at any time from the dashboard.

terminal
curl https://hermesagentmail.com/api/v1/inboxes \
  -H "Authorization: Bearer hm_live_..."
ScopeGrants
email:readList inboxes, list messages, and read full message content.
email:draftDraft replies to inbound messages.
email:send_requestRequest that a drafted reply is sent.
email:writeFull email write access, including direct sends where allowed.
inbox:writeCreate and manage inboxes.

Quickstart

The full loop with curl: create an inbox, list inbound messages, read one, draft a reply, then request that it is sent. You need a key with the inbox:write, email:read, email:draft, and email:send_request scopes.

1. create an inbox
curl -X POST https://hermesagentmail.com/api/v1/inboxes \
  -H "Authorization: Bearer $HERMES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "local_part": "research",
    "agent_name": "Research Agent"
  }'
2. list inbound messages
curl "https://hermesagentmail.com/api/v1/messages?limit=50" \
  -H "Authorization: Bearer $HERMES_API_KEY"
3. read a full message
curl https://hermesagentmail.com/api/v1/messages/MESSAGE_ID \
  -H "Authorization: Bearer $HERMES_API_KEY"
4. draft a reply
curl -X POST https://hermesagentmail.com/api/v1/messages/MESSAGE_ID/reply \
  -H "Authorization: Bearer $HERMES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Thanks for reaching out — here is the summary you asked for." }'
5. request send
curl -X POST https://hermesagentmail.com/api/v1/outbound/OUTBOUND_MESSAGE_ID/request-send \
  -H "Authorization: Bearer $HERMES_API_KEY"

If the inbox is in required approval mode, step 5 returns approval_required and the message waits in the dashboard approval queue. In auto mode it returns sent.

Inboxes API

An inbox is an address plus an agent identity and an approval mode. Reserved local parts (support, billing, abuse, postmaster, admin, noreply, and other system names) cannot be claimed.

GET /api/v1/inboxes
curl https://hermesagentmail.com/api/v1/inboxes \
  -H "Authorization: Bearer $HERMES_API_KEY"

# Requires scope: email:read.
{
  "data": [
    {
      "id": "8f4c1a2e-...",
      "address": "research@hermesagentmail.com",
      "agent": "Research Agent",
      "status": "active",
      "approval_mode": "required",
      "received": 128,
      "sent": 31,
      "created_at": "2026-06-30T09:12:00Z"
    }
  ]
}
POST /api/v1/inboxes
curl -X POST https://hermesagentmail.com/api/v1/inboxes \
  -H "Authorization: Bearer $HERMES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "local_part": "research",
    "agent_name": "Research Agent"
  }'

# Requires scope: inbox:write. Returns 201.
{
  "data": {
    "id": "8f4c1a2e-...",
    "address": "research@hermesagentmail.com",
    "agent": "Research Agent",
    "status": "active",
    "approval_mode": "required",
    "created_at": "2026-07-05T02:41:00Z"
  }
}

Errors: 400 invalid_local_part, 400 reserved_address, 400 address_taken, and 402 quota_exceeded when your plan has no inbox slots left. Every new inbox starts as approval_mode: "required" — a human approves every send. On paid plans, an inbox earns auto-send in the dashboard after 10 approved sends with no bounces or complaints, and loses it automatically if either occurs later.

Messages API

List inbound messages across your inboxes, or filter to a single thread. The list endpoint returns metadata and a snippet only; fetch a message by id for the full decrypted body.

GET /api/v1/messages
curl "https://hermesagentmail.com/api/v1/messages?limit=50&thread_id=THREAD_ID" \
  -H "Authorization: Bearer $HERMES_API_KEY"

# Requires scope: email:read. Metadata + snippet only.
{
  "data": [
    {
      "id": "3b9d7c10-...",
      "thread_id": "5a2e8f44-...",
      "address": "research@hermesagentmail.com",
      "from": "customer@example.com",
      "subject": "Invoice question",
      "snippet": "Can you resend my latest invoice?",
      "created_at": "2026-07-04T12:00:00Z"
    }
  ]
}
GET /api/v1/messages/:id
curl https://hermesagentmail.com/api/v1/messages/MESSAGE_ID \
  -H "Authorization: Bearer $HERMES_API_KEY"

# Requires scope: email:read. Full message.
{
  "data": {
    "id": "3b9d7c10-...",
    "thread_id": "5a2e8f44-...",
    "from": "customer@example.com",
    "subject": "Invoice question",
    "text": "Can you resend my latest invoice?",
    "html": "<p>Can you resend my latest invoice?</p>",
    "headers": { "message-id": "<abc@example.com>" },
    "attachments": [
      { "filename": "invoice.pdf", "content_type": "application/pdf", "size": 48213 }
    ],
    "created_at": "2026-07-04T12:00:00Z"
  }
}

Message bodies are stored encrypted and decrypted on read. Attachment entries are metadata only — filenames, content types, and sizes.

Replies & sending

Sending is a two-step flow: draft a reply, then request that it is sent. Human approval before send is the default everywhere. A draft never leaves the platform until it is either approved in the dashboard or the inbox is in auto approval mode.

POST /api/v1/messages/:id/reply
curl -X POST https://hermesagentmail.com/api/v1/messages/MESSAGE_ID/reply \
  -H "Authorization: Bearer $HERMES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "plain text reply" }'

# Requires scope: email:draft. Returns 201.
{
  "data": {
    "outbound_message_id": "c71f0b3d-...",
    "approval_required": true,
    "status": "pending_approval"
  }
}
POST /api/v1/outbound/:id/request-send
curl -X POST https://hermesagentmail.com/api/v1/outbound/OUTBOUND_MESSAGE_ID/request-send \
  -H "Authorization: Bearer $HERMES_API_KEY"

# Requires scope: email:send_request.
{
  "data": {
    "status": "sent" | "approval_required" | "blocked",
    "reason": "optional explanation when blocked"
  }
}

sent means the message went out. approval_required means it is waiting in the dashboard approval queue. blocked means policy stopped the send — for example a recipient on the suppression list (maintained from bounces and complaints) or an exhausted outbound quota — with the reason field explaining why.

Webhooks

Configure endpoints in the dashboard under Webhooks. When an inbound email is stored, we POST an email.receivedevent to each endpoint, signed with that endpoint's whsec_ secret. Verify the signature, respond with a 2xx quickly, and do the heavy work asynchronously.

HeaderValue
X-HermesAgentMail-Eventemail.received
X-HermesAgentMail-TimestampUnix timestamp in seconds when the delivery was signed.
X-HermesAgentMail-SignatureHex HMAC-SHA256 of {timestamp}.{rawBody}using your endpoint's whsec_ secret.
webhook payload
{
  "event": "email.received",
  "message_id": "3b9d7c10-...",
  "thread_id": "5a2e8f44-...",
  "workflow_id": "8f4c1a2e-...",
  "address": "research@hermesagentmail.com",
  "from": "customer@example.com",
  "subject": "Invoice question",
  "snippet": "Can you resend my latest invoice?",
  "requires_human_approval": true,
  "created_at": "2026-07-04T12:00:00Z"
}
verify.ts
import crypto from 'node:crypto'

export function verifyHermesWebhook({
  rawBody,
  timestamp,
  signature,
  secret, // Your whsec_ endpoint secret from the dashboard.
}: {
  rawBody: string
  timestamp: string
  signature: string
  secret: string
}): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(signature, 'hex')
  const b = Buffer.from(expected, 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

// Usage in a route handler: read the raw request body (before any
// JSON parsing) and the two X-HermesAgentMail-* headers, then reject
// the request with a 401 if verification fails.

MCP server

HermesAgentMail exposes a Model Context Protocol server over streamable HTTP at https://hermesagentmail.com/api/mcp, authenticated with the same hm_live_… API keys. Agents get email as first-class tools, and human approval is still required before send unless the inbox approval mode is auto. MCP access is available on Pro and Business plans.

mcp client config
{
  "mcpServers": {
    "hermesagentmail": {
      "url": "https://hermesagentmail.com/api/mcp",
      "headers": { "Authorization": "Bearer hm_live_..." }
    }
  }
}
ToolWhat it does
list_inboxesList the inboxes in your workspace.
create_inboxCreate a new agent inbox.
list_emailsList recent inbound messages with metadata and snippets.
get_emailFetch a full message, including body and attachments metadata.
search_threadSearch messages within a thread.
draft_replyDraft a reply to an inbound message.
request_sendRequest that a drafted reply is sent (subject to approval).
send_emailSend an email where policy allows it.
mark_handledMark a message or thread as handled.

Use with Hermes

Running the Hermes agent? Give it a real inbox in a couple of minutes. There are two ways to install — both need an API key from your dashboard. Every new inbox is approval-required, so your agent drafts and sends without ever being able to send something you did not allow.

Option A — MCP (zero code, recommended)

Hermes is an MCP client. Point it at the HermesAgentMail MCP server by adding this to ~/.hermes/config.yaml, then set HERMES_AGENT_MAIL_API_KEY and restart Hermes. Tools appear as mcp_hermes_agent_mail_*.

~/.hermes/config.yaml
mcp_servers:
  hermes-agent-mail:
    url: "https://www.hermesagentmail.com/api/mcp"
    headers:
      Authorization: "Bearer ${HERMES_AGENT_MAIL_API_KEY}"
    enabled: true

Or add it from the CLI, then restart your session:

terminal
export HERMES_AGENT_MAIL_API_KEY="hm_live_..."
hermes mcp add hermes-agent-mail \
  --url https://www.hermesagentmail.com/api/mcp

Option B — Native plugin (drop-in, no MCP needed)

Prefer native tool names, or not using MCP? Install the standalone Hermes plugin with one command. It adds six agentmail_* tools plus a behavior skill, and depends only on the Python standard library.

terminal
curl -fsSL https://www.hermesagentmail.com/hermes/install.sh | bash
# add HERMES_AGENT_MAIL_API_KEY=hm_live_... to ~/.hermes/.env
hermes plugins enable hermes-agent-mail
# restart Hermes — you'll see the "agentmail" toolset
ToolWhat it does
agentmail_list_inboxesList the agent inboxes in your workspace.
agentmail_create_inboxCreate name@hermesagentmail.com.
agentmail_list_emailsList recent inbound messages (metadata).
agentmail_get_emailFull decrypted message and attachments.
agentmail_draft_replyDraft a threaded reply (does not send).
agentmail_request_sendSend it, or route to human approval.

Prefer to have an AI agent set it up?

Copy one of these prompts and paste it into your AI agent. For Option A, ask Hermes itself (it has terminal and file tools). For Option B, use a coding agent with shell access like Cursor or Claude Code. Replace hm_live_... with your key from the dashboard.

Prompt for Hermes — configure MCP (Option A):

paste into Hermes
Configure the HermesAgentMail email integration for yourself.
Add an MCP server to ~/.hermes/config.yaml under "mcp_servers" named
"hermes-agent-mail" with:
  url: https://www.hermesagentmail.com/api/mcp
  headers.Authorization: "Bearer ${HERMES_AGENT_MAIL_API_KEY}"
  enabled: true
Do not remove or overwrite any existing mcp_servers entries. Then make
sure HERMES_AGENT_MAIL_API_KEY=hm_live_... is set in my ~/.hermes/.env
(create it if missing). Finally, tell me to restart my Hermes session so
the email tools load.

Prompt for Cursor / Claude Code — install the plugin (Option B):

paste into Cursor or Claude Code
Install the HermesAgentMail plugin into my Hermes agent.
1. Run: curl -fsSL https://www.hermesagentmail.com/hermes/install.sh | bash
   (this downloads the plugin into ~/.hermes/plugins/hermes-agent-mail)
2. Add HERMES_AGENT_MAIL_API_KEY=hm_live_... to ~/.hermes/.env
3. Run: hermes plugins enable hermes-agent-mail
4. Confirm the "agentmail" toolset loaded (hermes plugins list) and tell me
   to restart Hermes.

Once connected, try a first task with your agent:

first task
Check my HermesAgentMail inboxes, read the most recent email, and draft a
reply. Request the send — if it needs approval, tell me and I'll approve it
in the dashboard.

Waking the agent on new email

To have inbound mail wake your agent instead of polling, add a webhook endpoint in the dashboard (Webhooks) pointing at your Hermes gateway. We deliver a signed email.received event — see the Webhooks section for the payload and signature verification.

Errors

Every error response uses the same envelope, with a stable code and a human-readable message.

error envelope
{
  "error": {
    "code": "quota_exceeded",
    "message": "Your plan's inbox limit has been reached. Upgrade to add more inboxes."
  }
}
StatusCodeMeaning
400invalid_local_partThe requested local part contains invalid characters.
400reserved_addressThe address is reserved by the platform (support@, abuse@, postmaster@, …).
400address_takenThe address is already claimed by another workspace.
401unauthorizedThe API key is missing, malformed, or revoked.
402quota_exceededYour plan quota (inboxes or monthly emails) has been reached.
403insufficient_scopeThe API key does not have the scope required for this endpoint.
404not_foundThe resource does not exist or belongs to another workspace.
429rate_limitedToo many requests. Back off and retry.

Rate limits & quotas

Requests are rate limited per key; back off and retry on 429. Monthly email quotas count both inbound and outbound messages, and message retention varies by plan. When a quota is exhausted the API returns 402 quota_exceeded.

PlanPriceInboxesEmails / mo (in / out)RetentionMCP
Free$0/mo175 / 257 daysIncluded
Starter$5/mo3750 / 25030 daysIncluded
Pro$14.99/mo103,750 / 1,25090 daysIncluded
Business$99/mo10037,500 / 12,5001 yearIncluded + team

Want early access?

HermesAgentMail is in early access. Join the waitlist to get an API key when workspaces open up.

Join the waitlist