Email for AI agents

How to Build an AI Email Agent (on a Real Inbox API)

Nico JaroszewskiFounder, AutoEmail8 min read
ai email agentemail automation apiemail for ai agentsagent api

An AI email agent is a loop, not a model: read the inbox, decide what each message needs, draft in the owner's voice, pass a safety gate, learn from corrections, and never exceed its budget. This guide builds that loop end to end on AutoEmail's REST API - every endpoint, field, and status code below is real and testable against the live API reference, with the full OpenAPI 3.1 spec at /openapi.json.

The short answer

Give the agent one Bearer API key scoped to specific mailboxes. It discovers its scope with GET /me, polls GET /emails?status=pending, drafts with POST /emails/{id}/reply (mode: "generate"), and - on a human-in-the-loop key - every write lands as a pending draft a person approves in the dashboard. Corrections become lessons the next draft uses.

What is an AI email agent, exactly?

An AI email agent is a program that operates a mailbox the way an assistant would: it reads what arrives, understands what each message is asking, writes a contextual reply in the right voice, and acts - sends, queues, declines, escalates. The critical word is operates. A vacation responder fires fixed text; a transactional API (Resend, SendGrid) pushes outbound mail one way. An agent needs the whole inbox: inbound reading, threading, drafting, sending, and - if it is going near real customers - a human checkpoint. For the conceptual tour of that distinction, see what a real agent inbox needs; this post is the build.

Everything below uses AutoEmail's Agent REST API: token-authenticated JSON over HTTPS, base URL https://courteous-gopher-315.eu-west-1.convex.site/api/v1 for hosted autoemail.dev accounts. Auth is one header on every call:

export BASE="https://courteous-gopher-315.eu-west-1.convex.site/api/v1"
export KEY="ak_live_..."   # created in the AutoEmail dashboard, shown once

The architecture: one loop, six capabilities

Every useful email agent - support triage bot, outreach runner, executive assistant - is the same loop wearing different prompts:

Loop stageWhat it meansEndpoint(s)
DiscoverWho am I? Which mailboxes may I touch?GET /me, GET /accounts
ReadWhat arrived? What does the thread say?GET /emails, GET /emails/{id}, GET /search, GET /threads
ActDraft a reply, compose new mail, run outreachPOST /emails/{id}/reply, POST /emails/send, POST /outreach/batch
GateDoes a human approve this send?key mode + POST /emails/{id}/approve / .../decline
LearnWhat did the human change, and why?GET/POST /accounts/{businessId}/lessons
ThrottleAm I inside my quota and rate limits?GET /usage, Idempotency-Key header

The rest of this guide walks the loop in order.

Step 1: Discover scope with GET /me

Never hardcode account ids. The first call an agent makes tells it its identity, its mode, and exactly which mailboxes its key may touch:

curl -s "$BASE/me" -H "Authorization: Bearer $KEY"
{
  "keyId": "kd7...",
  "name": "Support Agent",
  "mode": "human_in_the_loop",
  "allowedAccounts": [
    { "businessId": "jd72...", "domain": "acme.com", "email": "support@acme.com", "name": "Acme Support" }
  ]
}

Two fields drive all downstream behavior. allowedAccounts is the key's allowlist - a request for any other account returns 403 business_not_allowed, so the blast radius of a leaked or confused agent is capped at what you granted. And mode is the safety architecture in one enum, which Step 4 unpacks.

Step 2: Read the inbox (the triage poll)

The agent's heartbeat is a filtered list poll. status=pending returns exactly the emails awaiting action:

curl -s "$BASE/emails?businessId=$BIZ&status=pending&pageSize=50" \
  -H "Authorization: Bearer $KEY"

Each item in the paginated response (items, page, pageSize, total, totalPages) carries what a triage decision needs: senderEmail, senderName, subject, receivedAt, a spamScore between 0 and 1, and the resolved effectiveReplyTo / effectiveReplySubject a reply would actually use - which matters for contact-form relays, where the mailbox that delivered the email is not the human who wrote it. List items are deliberately lean; GET /emails/{id} returns the full body, cc/bcc, attachments, and the latest draft. For keyword recall across the whole allowlist, GET /search?q=refund%20invoice does case-insensitive full-text search.

A sane triage policy in the LLM prompt: high spamScore gets left alone (or PATCHed to spam, which teaches the filter); routine questions get drafted; anything involving money, commitments, or an upset customer gets drafted with extra constraints - the gate in Step 4 catches what slips.

Step 3: Draft the reply (brief in, draft out)

The reply endpoint has two modes. mode: "generate" asks AutoEmail to write the copy - with the business's voice, knowledge, and accumulated lessons - optionally steered per call:

curl -s -X POST "$BASE/emails/$EMAIL_ID/reply" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-$EMAIL_ID-1" \
  -d '{
    "mode": "generate",
    "brief": "Confirm the June invoice was resent this morning and apologize for the duplicate.",
    "tone": "warm, brief",
    "constraints": "Do not promise any refund or credit."
  }'

On a human-in-the-loop key the response is a queued draft, not a sent email:

{ "status": "pending_approval", "emailId": "jd91...", "draftId": "jx3..." }

The alternative is mode: "final", where your agent supplies the finished body itself - the right choice when the LLM composing the copy is your own. Composing to a NEW recipient works the same way via POST /emails/send (final mode requires businessId, to, subject, body; generate mode takes a brief). Every accepted reply, send, approve, and outreach recipient meters exactly 1 unit of email quota - and a generation that fails after metering is refunded.

Step 4: The gate - who presses send?

The key's mode decides what a write actually does, and it is the most important architectural choice in the whole build:

  • human_in_the_loop (the default): every reply or send the agent makes becomes a pending_approval draft. A person reviews it in the AutoEmail dashboard - or another integration approves it explicitly. The agent itself calling POST /emails/{id}/approve is rejected with 403 mode_not_allowed; declining via POST /emails/{id}/decline is always allowed, because declining is safe.
  • full_autonomous: writes send immediately over SMTP, and POST /emails/{id}/approve works, returning { "status": "sent", "emailId": "...", "smtpMessageId": "<...>" }. Even here, generated replies default to draft-only unless the call passes send: true - autonomy is opt-in per call, not just per key.

The honest engineering advice: start every agent human-in-the-loop. An LLM will eventually write something confidently wrong, and the difference between an incident and a non-event is whether a person stood between the draft and the customer. The human-in-the-loop pattern is not a training-wheels phase - for customer-facing mail it is the design.

Step 5: Close the learning loop with lessons

This is the step most agent builds skip, and it is why they plateau. When a human declines a draft with feedback, that correction is stored as a lesson scoped to the business - and injected into every future generation for that inbox. Your agent can read and write the same memory over the API:

# What has this inbox learned so far?
curl -s "$BASE/accounts/$BIZ/lessons" -H "Authorization: Bearer $KEY"
# -> { "lessons": [ { "id": "...", "title": null, "content": "Sign off as 'Nico', never 'the team'", "source": "...", "createdAt": 1752... } ], "spamLessons": [ ... ] }

# Teach a rule explicitly (content required, max 2000 chars; 50-lesson cap per account)
curl -s -X POST "$BASE/accounts/$BIZ/lessons" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Pricing questions", "content": "Never quote custom pricing in email; offer a call with Nico instead." }'

The compounding effect is the point: the tenth week's drafts are measurably closer to the owner's voice than the first week's, because every edit and decline was banked instead of evaporating. The spam filter learns the same way - PATCH /emails/{id} moving a message to or from spam teaches it, exactly like the dashboard buttons.

Step 6: Make it production-grade (quota, idempotency, errors)

Three habits separate a demo from an agent you can leave running:

Self-throttle before the wall. GET /usage returns { periodStart, periodEnd, used, includedQuota, topUpQuota, remaining }. Check remaining before batch work; when quota runs out, writes fail fast with 402 quota_exceeded and nothing is sent.

Idempotency on every write. All write endpoints accept an Idempotency-Key header. A retry with the same key and body replays the original response verbatim (Idempotency-Replayed: true) instead of double-sending - which is what you want when your agent's network call times out mid-reply.

Branch on error codes, not messages. Every error is { "error": "machine_code", "message": "human text" }: 401 for bad keys, 403 business_not_allowed / mode_not_allowed, 404 without existence leaks, 422 invalid_* validation, 429 rate_limited per key and route. Parse error, never message.

For outreach-style agents there is one more primitive: POST /outreach/batch takes up to 100 recipients per call with quota fail-fast, a dedupe window, a daily cap, and drip scheduling - and GET /emails?outreachBatchId=... polls the per-recipient outcomes.

Wiring it to an actual LLM

Nothing above requires a framework: an agent is a system prompt, a loop, and an HTTP client. Point your LLM at the OpenAPI 3.1 spec (GPT Actions and most tool-use runtimes consume it directly - here is the ChatGPT walkthrough), or expose the endpoints as tools by hand: list_pending_emails, read_email, draft_reply, teach_lesson, check_usage. The prompt encodes your triage policy; the key's mode and allowlist encode your safety policy; the lessons endpoints encode your taste. On AutoEmail's catalog, the agent API ships with the Team plan in human-in-the-loop mode, and the Agentic plan adds full-autonomous keys - see pricing for the current numbers.

Want your agent drafting on a real inbox today - with you on the send button?

Start free

Bottom line

Building an AI email agent is not a model problem - it is an inbox-access and governance problem. Solve it in this order: a scoped key (GET /me tells the agent its box), a triage poll (GET /emails?status=pending), generated drafts steered by briefs and constraints, an approval gate that defaults to human-in-the-loop, a lessons loop so corrections compound, and quota-aware, idempotent writes so the thing can run unattended without surprises. Every piece of that is a documented HTTP call - which means the agent you prototype with curl this afternoon is the same one you ship.

Frequently asked questions

An AI email agent is a program - usually an LLM plus a loop of API calls - that operates a real mailbox: it reads incoming email, decides what each message needs, drafts a reply in the owner's voice, and either sends it or queues it for human approval. It differs from an autoresponder (fixed text, no understanding) and from a sending API (outbound only, no inbox).

autoemail

Put AI on every reply. Keep yourself in the loop.

Connect one inbox, watch AutoEmail draft every reply, and approve before anything sends. Free to start, no card required.

30-day money-back guarantee

Try any paid plan risk-free. If AutoEmail is not saving you time inside 30 days, email us and we refund you in full - no forms, no friction.