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
- 1Email arrives at
your-inbox@hermesagentmail.com. - 2Resend receives the message and delivers a verified webhook to us.
- 3The event lands in a durable queue for reliable processing.
- 4A worker stores the message encrypted (AES-256-GCM at the application layer).
- 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.
curl https://hermesagentmail.com/api/v1/inboxes \
-H "Authorization: Bearer hm_live_..."| Scope | Grants |
|---|---|
email:read | List inboxes, list messages, and read full message content. |
email:draft | Draft replies to inbound messages. |
email:send_request | Request that a drafted reply is sent. |
email:write | Full email write access, including direct sends where allowed. |
inbox:write | Create 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.
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"
}'curl "https://hermesagentmail.com/api/v1/messages?limit=50" \
-H "Authorization: Bearer $HERMES_API_KEY"curl https://hermesagentmail.com/api/v1/messages/MESSAGE_ID \
-H "Authorization: Bearer $HERMES_API_KEY"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." }'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.
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"
}
]
}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.
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"
}
]
}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.
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"
}
}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.
| Header | Value |
|---|---|
X-HermesAgentMail-Event | email.received |
X-HermesAgentMail-Timestamp | Unix timestamp in seconds when the delivery was signed. |
X-HermesAgentMail-Signature | Hex HMAC-SHA256 of {timestamp}.{rawBody}using your endpoint's whsec_ secret. |
{
"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"
}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.
{
"mcpServers": {
"hermesagentmail": {
"url": "https://hermesagentmail.com/api/mcp",
"headers": { "Authorization": "Bearer hm_live_..." }
}
}
}| Tool | What it does |
|---|---|
list_inboxes | List the inboxes in your workspace. |
create_inbox | Create a new agent inbox. |
list_emails | List recent inbound messages with metadata and snippets. |
get_email | Fetch a full message, including body and attachments metadata. |
search_thread | Search messages within a thread. |
draft_reply | Draft a reply to an inbound message. |
request_send | Request that a drafted reply is sent (subject to approval). |
send_email | Send an email where policy allows it. |
mark_handled | Mark a message or thread as handled. |
create_task | Create 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": {
"code": "quota_exceeded",
"message": "Your plan's inbox limit has been reached. Upgrade to add more inboxes."
}
}| Status | Code | Meaning |
|---|---|---|
400 | invalid_local_part | The requested local part contains invalid characters. |
400 | reserved_address | The address is reserved by the platform (support@, abuse@, postmaster@, …). |
400 | address_taken | The address is already claimed by another workspace. |
401 | unauthorized | The API key is missing, malformed, or revoked. |
402 | quota_exceeded | Your plan quota (inboxes or monthly emails) has been reached. |
403 | insufficient_scope | The API key does not have the scope required for this endpoint. |
404 | not_found | The resource does not exist or belongs to another workspace. |
429 | rate_limited | Too 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.
| Plan | Price | Inboxes | Emails / mo (in / out) | Retention | MCP |
|---|---|---|---|---|---|
| Free | $0/mo | 1 | 25 / 10 | 7 days | — |
| Starter | $9/mo | 5 | 1,000 / 250 | 30 days | — |
| Pro | $29/mo | 25 | 10,000 / 2,500 | 90 days | Included |
| Business | $99/mo | 100 | 50,000 / 10,000 | 1 year | Included + team |
Want early access?
HermesAgentMail is in early access. Join the waitlist to get an API key when workspaces open up.
