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",
    "approval_mode": "required"
  }'
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",
      "creates_tasks": true,
      "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",
    "approval_mode": "required"
  }'

# 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. approval_mode is either "required" (default, a human approves every send) or "auto".

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.
create_taskCreate a follow-up task from a message.

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/mo125 / 107 days
Starter$9/mo51,000 / 25030 days
Pro$29/mo2510,000 / 2,50090 daysIncluded
Business$99/mo10050,000 / 10,0001 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