SuperAPI AI Directory

agentmail

Agents & models

View repository

Updated 10-16 05:52

About this skill

AgentMail

An API-first mailbox built for agents. Unlike one-way senders (Resend, SendGrid, SES), an AgentMail inbox both sends and receives, so an agent can drive a real email round trip end to end: sign up for a service, wait for the verification mail, pull the code out, and continue.

Credentials

AGENTMAIL_API_KEY is exported globally from ~/.zshenv, so it is already in the environment of any shell an agent spawns. Confirm before doing anything else:

[ -n "$AGENTMAIL_API_KEY" ] && echo "key present" || echo "key missing"

If it is missing, the key lives in ~/.zshenv; source it rather than asking the user to paste it again. Refer to the key as $AGENTMAIL_API_KEY in every command. Keep the literal value out of transcripts, commit messages, source files, and any .env that git tracks.

Auth is a bearer header on every REST call:

-H "Authorization: Bearer $AGENTMAIL_API_KEY"

Resolve the inbox first

Every send, list, and reply is scoped to an inbox_id (which is just the inbox's email address, such as agent@agentmail.to). Resolve it once at the start of a task, in this order, and reuse it:

  1. $AGENTMAIL_INBOX_ID if the environment already sets it.
  2. GET /v0/inboxes and take the address you want.
  3. Create one: POST /v0/inboxes with a client_id, which makes the call idempotent so re-running a script reuses the same inbox instead of burning another one against the plan limit.
  4. If steps 2 and 3 both return 403 missing_permission, the key is inbox-scoped and cannot enumerate or create. Fall back to GET /v0/threads, which is org-wide and returns an inbox_id on every thread. With an empty inbox and no thread to read, ask the user for the address rather than guessing.
# 1-2: list
curl -s https://api.agentmail.to/v0/inboxes \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"

# 3: create, idempotent on client_id
curl -s -X POST https://api.agentmail.to/v0/inboxes \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" -H "Content-Type: application/json" \
  -d '{"username":"altumo-test","client_id":"altumo-test-inbox"}'

# 4: recover the address from any existing thread
curl -s "https://api.agentmail.to/v0/threads?limit=1" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"

A per-purpose inbox with a stable client_id beats a fresh inbox per run. The free plan allows 3 inboxes and 3,000 messages a month, so inboxes are the scarce resource and messages are not.

Send

curl -s -X POST "https://api.agentmail.to/v0/inboxes/$INBOX/messages/send" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" -H "Content-Type: application/json" \
  -d '{"to":["someone@example.com"],"subject":"Hello","text":"Plain body","html":"<p>Plain body</p>"}'

Returns {"message_id":"...","thread_id":"..."}. Always send text alongside html; an html-only message reads as spam to filters and as nothing to plain-text clients.

Receive

Receiving means one of three things. Pick by what the situation actually allows:

  • Polling is the default for local work and tests. No public URL, no server, works everywhere.
  • WebSocket for a long-lived local process that wants sub-second delivery without exposing a port.
  • Webhook only when a publicly reachable HTTPS endpoint already exists or the task is to build one. See references/events.md.

The common case, waiting on a verification or reset mail, is one command:

scripts/wait-for-email.sh --inbox "$INBOX" --from noreply@example.com --timeout 120

It polls until a matching message lands, prints the full message as JSON, and exits non-zero on timeout. --subject, --after, and --extract-code narrow or post-process the result; --help lists the flags. Reading the mail is GET /v0/inboxes/{inbox_id}/messages, and the flags map onto its from, subject, and after filters.

Read a message body from extracted_text (the new content, with the quoted trail stripped) and fall back to text. Use text only when the quoted history is the point.

Reply in thread

curl -s -X POST "https://api.agentmail.to/v0/inboxes/$INBOX/messages/$MESSAGE_ID/reply" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" -H "Content-Type: application/json" \
  -d '{"text":"Thanks, got it.","html":"<p>Thanks, got it.</p>"}'

Replying through this route keeps threading and headers correct. A fresh send with Re: glued onto the subject starts a new thread and breaks the conversation for the recipient.

Sending on the user's behalf

Mail to a real person leaves the machine and cannot be recalled. Show the user the recipient, subject, and body, and get an explicit go-ahead before the first such send. Test mail to an inbox you control needs no such check. Once the user approves a specific send, do not re-ask for each retry of that same message.

Building it into a project

Reach for an SDK over curl when the email flow becomes application code rather than a one-off shell step: npm install agentmail or pip install agentmail, both of which read AGENTMAIL_API_KEY from the environment on construction. The TypeScript client is AgentMailClient, the Python one is AgentMail. Method signatures, pagination, attachments, labels, drafts, domains, and pods live in references/rest-api.md.

For a project of the user's own, put AGENTMAIL_API_KEY in the project's gitignored .env and read it from there. The global export is for agent work, not for shipped code that other people will run.

Gotchas

  • The message list includes mail the inbox sent. Sent copies carry the sent label and sit alongside received mail in GET .../messages, so a naive "wait for a message" matches the agent's own send within milliseconds. Skip anything labelled sent when waiting for something to arrive. wait-for-email.sh already does.
  • An inbox cannot mail itself. A message addressed from an inbox back to that same inbox is recorded as sent and never loops around as a delivery, so a round trip needs a second address.
  • Message ids are angle-bracketed, like <...@email.amazonses.com>. URL-encode the whole id before putting it in a path, or the fetch 404s.
  • Bounces and complaints are permanent. A bounced or spam-reported address is blocked for the whole account. Never loop a send against an address that just bounced, and keep the bounce rate under 4% or the account goes under review.
  • message.received is the only event carrying the full thread and message. Every other event is metadata, so a handler that needs the body must fetch it.
  • 403 missing_permission is a key problem, not a code problem. The error names the exact permission. An inbox-scoped or whitelisted key cannot be widened from itself; a broader key has to come from the console at https://console.agentmail.to.
  • 429 carries Retry-After. Honour it and back off exponentially instead of retrying tight.
  • One SPF record per domain. Merge, do not append a second: v=spf1 include:spf.agentmail.to include:other.com ~all.

Reference