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.
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.
How send idempotency works: canonical payload hashing, the 30-day replay window, conflict versus expiry, and why retrying an unknown outcome is unsafe.