API overview · MachineMail Docs
Base URL
MachineMail is an email API for AI agents. The /v0 HTTP API is wire-compatible with AgentMail’s /v0 API: the same paths, snake_case field names, enums and pagination. MachineMail adds trust reports, ephemeral and catch-all inboxes, delivery replay, an event log and live event streams. See AgentMail compatibility.
The endpoint reference is generated from the OpenAPI document, served at GET /v0/openapi.json.
https://api.machinemail.dev
| Surface | URL |
|---|---|
| REST API | https://api.machinemail.dev/v0/... |
| Remote MCP server | https://mcp.machinemail.dev/mcp ( MCP tools) |
| Server-sent events | https://api.machinemail.dev/v0/events/stream |
| WebSocket | wss://api.machinemail.dev/v0/ws |
| Agent onboarding (auth.md) | https://api.machinemail.dev/auth.md |
Agent inboxes on the shared domain have addresses like agent@machinemail.dev.
Versioning
Every resource endpoint sits under /v0. There is no version header. Health checks (/health), the MCP endpoint (/mcp), the agent onboarding endpoints (/agent/*, /oauth2/*, /.well-known/*) and the dashboard session endpoints (/auth/*) are unversioned.
Authentication
Send an API key as a bearer token:
curl https://api.machinemail.dev/v0/inboxes \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Keys look like mm_live_ followed by 32 base62 characters. A key belongs to an organization and is scoped to the whole organization, one pod, or one inbox. It can carry a permission allowlist; without one it can do everything its scope allows. Agent access tokens issued through the auth.md flow are accepted in the same header. See Authentication.
A missing or invalid credential returns 401 with a WWW-Authenticate: Bearer resource_metadata="https://api.machinemail.dev/.well-known/oauth-protected-resource" header. A resource outside the key’s scope returns 404, not 403.
Requests
- Request bodies are JSON objects. The server parses the body as JSON whatever the
Content-Type; sendContent-Type: application/jsonanyway. An empty body counts as{}. Anything that is not a JSON object returns400withRequest body must be a JSON object. - Field names are snake_case. Body fields are also accepted in camelCase (
display_nameordisplayName). - Timestamps are ISO 8601 strings in UTC, for example
2026-10-01T12:00:00.000Z. - Boolean query parameters are true when the value is empty,
trueor1, and false otherwise. - List query parameters (such as
labelsorevent_types) can be repeated (labels=A&labels=B), comma-separated (labels=A,B) or a JSON array (labels=["A","B"]). Thename[]form also works. inbox_idis the inbox’s email address. Other IDs are prefixed:org_,pod_,thd_,msg_,drf_,whk_,dom_,key_,dlv_,evt_,att_. IDs sort by creation time.
Responses
- Successful responses are JSON with snake_case fields, except raw message, attachment, zone file, export and stream endpoints.
- Creating an inbox, draft, webhook, domain, pod, API key or suppression returns
201. When aclient_idmatches an existing resource the existing one comes back with200(see Idempotency). Adding a list entry returns200. - Sending (send, reply, reply-all, forward, draft send) returns
201withmessage_id,thread_idandtimestamp. - Deleting returns
200with{\"ok\": true}. Revoking an agent registration returns the registration. - Optional fields with no value are left out of the JSON.
- Errors use one envelope:
{\"error\": {\"type\": "...", \"message\": "...", \"param\": "..."}}. See Errors.
Pagination
List endpoints take limit (default 50, max 500), page_token and ascending, and return count, limit and next_page_token. See Pagination.
Idempotency
There is no Idempotency-Key header. Instead, creates accept a client_id field:
| Resource | Endpoint | client_id behaviour |
|---|---|---|
| Inbox | POST /v0/inboxes |
dedupes |
| Draft | POST /v0/inboxes/{inbox_id}/drafts |
dedupes |
| Webhook | POST /v0/webhooks |
dedupes |
| Domain | POST /v0/domains |
dedupes |
| Pod | POST /v0/pods |
dedupes |
| API key | POST /v0/api-keys |
stored, but not deduped: every call creates a key |
When a resource in the organization already has the client_id, the API returns that resource with 200 instead of creating a new one (201). The request body is not compared, so a retry with different fields still returns the original resource. client_id values are unique per organization, not per pod or inbox. A request limited to a pod or an inbox only gets back a resource it could read: when the client_id belongs to a resource outside that pod or inbox, the API returns 409 conflict without that resource. A request is limited by a pod- or inbox-scoped API key, and also by a route under /v0/pods/{pod_id} or /v0/inboxes/{inbox_id} even with an organization key.
Sending mail is not idempotent: retrying messages/send after a timeout can send twice. To make a send safe to retry, create a draft with a client_id, then send the draft.
Request IDs
The API does not set or read a request ID header. When you report a problem, include the time, the method and path, and any resource IDs.
CORS
The API does not allow cross-origin browser requests: call it from your server, never from a web page that holds your API key.
Scoped routes
Every /v0 resource route is also served under /v0/pods/{pod_id}/..., which narrows the request to that pod. Webhook, list, metric, event and API key routes are also served under /v0/inboxes/{inbox_id}/..., narrowed to that inbox. The OpenAPI document describes the unprefixed form (and the prefixed form for lists). Creating an API key under a prefix scopes the new key to that pod or inbox.