Product

An 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

Test mode is free and cannot reach the internet.

Create an API key Read the docs