Skip to content
machinemail

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

text
https://api.machinemail.dev
SurfaceURL
REST APIhttps://api.machinemail.dev/v0/...
Remote MCP serverhttps://mcp.machinemail.dev/mcp (MCP tools)
Server-sent eventshttps://api.machinemail.dev/v0/events/stream
WebSocketwss://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:

shell
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; send Content-Type: application/json anyway. An empty body counts as {}. Anything that is not a JSON object returns 400 with Request body must be a JSON object.
  • Field names are snake_case. Body fields are also accepted in camelCase (display_name or displayName).
  • 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, true or 1, and false otherwise.
  • List query parameters (such as labels or event_types) can be repeated (labels=A&labels=B), comma-separated (labels=A,B) or a JSON array (labels=["A","B"]). The name[] form also works.
  • inbox_id is 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 a client_id matches an existing resource the existing one comes back with 200 (see Idempotency). Adding a list entry returns 200.
  • Sending (send, reply, reply-all, forward, draft send) returns 201 with message_id, thread_id and timestamp.
  • Deleting returns 200 with {"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:

ResourceEndpointclient_id behaviour
InboxPOST /v0/inboxesdedupes
DraftPOST /v0/inboxes/{inbox_id}/draftsdedupes
WebhookPOST /v0/webhooksdedupes
DomainPOST /v0/domainsdedupes
PodPOST /v0/podsdedupes
API keyPOST /v0/api-keysstored, 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.

Next