Same key, same body
Returns the original result. Nothing new is transmitted.
The replay window is 30 days from the first request.
Product
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.
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.
Every send carries an idempotency key, and the server hashes a canonical form of the payload against it.
Returns the original result. Nothing new is transmitted.
The replay window is 30 days from the first request.
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.
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.
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" message.receivedmessage.queuedmessage.acceptedmessage.delivery_updatedmessage.failedmessage.submission_unknownmessage.canceledmessage.quarantinedapproval.requestedapproval.approvedapproval.rejectedapproval.invalidatedinbox.createdinbox.updatedinbox.deleteddomain.updatedproject.sending_pausedwebhook.testA 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Use non-routable test inboxes to simulate inbound mail, exercise drafts, approvals and webhooks, and verify agent behavior before a live message goes out.
A routable test mailbox needs a message actually transmitted. Inject one instead: a CI job asserting received, parsed, threaded and replied.
How send idempotency works: canonical payload hashing, the 30-day replay window, conflict versus expiry, and why retrying an unknown outcome is unsafe.