API overview
Base URL, versioning, authentication, request and response conventions.
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, openapi/machinemail.yaml.
Base URL
| 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:
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.
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
Browser access is off unless the deployment lists allowed origins. When enabled, CORS allows credentials and the request headers Authorization, Content-Type, X-Recipient and Idempotency-Key, and exposes WWW-Authenticate.
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.