Product

An agent email API you can reason about

Scoped keys, sends that cannot double-fire, and an event log that distinguishes accepted from delivered.

The console and the API are the same pipeline — the same permissions, the same policy engine, the same idempotency. Anything a human can do in the console, a scoped credential can do over HTTP, and nothing can do more.

Sending a message

One POST per message, scoped to a project and an inbox. Recipients are objects, not strings, and an Idempotency-Key header is required — the request is rejected without one.

curl -X POST \
  "https://api.emailforagents.ai/v1/projects/$PROJECT_ID/inboxes/$INBOX_ID/messages" \
  -H "Authorization: Bearer $EFA_API_KEY" \
  -H "Idempotency-Key: 6f1c2b2e-3a7d-4a2f-9f4e-2b0d1c7a8e55" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{ "email": "ap@example.com", "name": "Accounts Payable" }],
    "subject": "Q3 invoice",
    "text": "Attached.",
    "reply_to_message_id": "msg_..."
  }'

The call returns 202 with a request id. Sending is asynchronous: acceptance by the provider and delivery to the recipient arrive later, as separate events. No TypeScript or Python SDK is published yet, so everything here is plain HTTP.

Retrying without sending twice

Every send carries an idempotency key, and the server hashes a canonical form of the payload against it.

Same key, same body

Returns the original result. Nothing new is transmitted.

The replay window is 30 days from the first request.

Same key, different body

Rejected as a conflict. This is almost always a bug in the caller, not a retry.

A record of used keys is kept for a year, so a very late replay is an expiry, not a send.

Unknown outcome

If a submission's fate cannot be established, the API says so with a message.submission_unknown event instead of guessing.

Reuse the same key to resolve it. A new key means a new intended message.

Knowing what happened

Poll the events endpoint with its cursor, or register a webhook and let it push. Both carry the same vocabulary.

curl "https://api.emailforagents.ai/v1/projects/$PROJECT_ID/events?cursor=$CURSOR" \
  -H "Authorization: Bearer $EFA_API_KEY"

Mail in and out

  • message.received
  • message.queued
  • message.accepted
  • message.delivery_updated
  • message.failed
  • message.submission_unknown
  • message.canceled
  • message.quarantined

Human decisions

  • approval.requested
  • approval.approved
  • approval.rejected
  • approval.invalidated

Configuration

  • inbox.created
  • inbox.updated
  • inbox.deleted
  • domain.updated
  • project.sending_paused
  • webhook.test

Webhooks that behave

A webhook endpoint must be public HTTPS. The delivery service resolves the address, pins the connection to a public IP, verifies TLS, and refuses redirects and private networks.

  • Signed, with a tolerance. Every delivery carries an HMAC signature over the timestamp and body. Reject anything more than 300 seconds old, then compare digests in constant time.

  • A published retry schedule. Up to 8 attempts, at 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours.

  • 4xx stops the retries. Except 408, 409, 425 and 429, which are treated as "try again". A 500 is always retried.

  • At-least-once, so deduplicate. Keep the event id you have already processed. Verify signatures and deduplicate events before acting on a callback.

Keys, limits and versioning

Keys

A key belongs to one project and can be narrowed to named inboxes. It is prefixed efa_test_ or efa_live_, so a key's blast radius is visible at a glance.

Secrets are digested and shown once. Start read-only and add send where you need it.

Rate limits

Roughly 1,200 requests a minute at the edge, and 600 a minute per principal and per workspace. Over the line you get a 429 and a Retry-After of 60 seconds.

The counters are per edge location and deliberately permissive, so treat these as a backstop rather than a quota to plan against.

Versioning

The current API version is 2026-09-19. The OpenAPI document is checked against the router in CI, so the schema and the deployed routes cannot drift apart.

An MCP endpoint speaks the same permissions — see the MCP server.

Guides for building on the API

What message.submission_unknown Actually Means

The send was attempted and nobody knows if it went out. What message.submission_unknown means, the silent path to it, and why resending is the wrong move.

10 min read

Agent Email Bounced 550 5.7.1: What It Means

Agent email bounced 550 5.7.1? Which variant you hit, why it is rarely your DNS on a shared domain, and why the agent must stop, not reword and resend.

12 min read

Retry, Backoff and Error Handling for Agent Email

Agent email error handling that sorts every error by whether a retry can help: a decision table, what backs off, and what goes to a human.

12 min read

Validating an Address Before Your Agent Sends

How to validate an email address before your agent sends: the syntax and DNS checks that are safe to run, and why probing the mailbox is not.

12 min read

Handling PDF Attachments in an Agent Inbox

How to handle PDF attachments in an AI agent's email: upload in three calls, pass only att_ IDs to the model, and read inbound files in code, not in context.

11 min read

Resume an Agent Workflow When a Reply Arrives

How to resume an agent workflow when an email reply arrives: key on thread_id, dedupe message.received by event ID, and catch replies that break the thread.

11 min read

Verify a Signed Email Webhook Before Trusting It

An unverified webhook is an open instruction channel into your agent. Verify the signature in order, and keep the payload from the model until it passes.

11 min read

Agent Email Polling vs Webhooks: A Decision Table

Agent email polling vs webhooks, decided on failure modes: cursor semantics, event-id dedup, and the exact 4xx codes that kill a delivery on the first try.

13 min read

Your Agent Sent the Same Email Twice. Why

Four reasons an agent sent the same email twice, each with a different fix: a keyless retry, a replayed event, two turns, and a second approval.

11 min read

My Agent Says It Sent It. Nobody Received It

Your AI agent says it sent an email and nobody received it. Work the states in order, learn which ones emit no event at all, and never retry one.

13 min read

Email MCP Server OAuth vs API Key: the 401 Trap

Email MCP server OAuth vs API key: a valid key gets 401 invalid_token at /mcp, not 403, so rotation never helps. What each credential carries.

10 min read

Is a mail.invalid Test Email Address Safe?

Why a mail.invalid test email address is safe: the .invalid TLD can never resolve, so a misrouted test send is unsendable rather than merely intercepted.

13 min read

Accepted vs Delivered: What an Email Status Proves

Queued, accepted, delivered, bounced, unknown: what each email status actually proves, what it does not, and the correct next action for every one of them.

12 min read

Agent Email API: Send, Receive and Reply in One Loop

Build the full two-way loop over REST: scoped keys, idempotent sends, the states a message moves through, inbound events, and replies that thread correctly.

10 min read

Keep an Agent's Replies in the Right Email Thread

How Message-ID, In-Reply-To and References decide threading, why subject matching breaks, and how to keep an agent reply in the right conversation.

11 min read

Handle Email Webhooks Without Processing Events Twice

Verify the signature against the raw body, deduplicate by event ID, enqueue durably, and keep a webhook retry from re-sending the email that produced it.

11 min read

Test an Agent's Email Without Sending Real Mail

Use non-routable test inboxes to simulate inbound mail, exercise drafts, approvals and webhooks, and verify agent behavior before a live message goes out.

9 min read

Test an Agent's Inbound Email Loop in CI

A routable test mailbox needs a message actually transmitted. Inject one instead: a CI job asserting received, parsed, threaded and replied.

13 min read

Idempotency Keys: Stop an Agent Sending Duplicate Emails

How send idempotency works: canonical payload hashing, the 30-day replay window, conflict versus expiry, and why retrying an unknown outcome is unsafe.

7 min read

Test mode is free and cannot reach the internet.

Create an API key Read the docs