---
title: "API overview"
description: "MachineMail API conventions: the base URL, /v0 versioning, authentication, requests and responses, pagination, idempotency, request IDs, CORS and scoped routes."
canonical: "https://machinemail.dev/docs/reference"
---

# API overview

> MachineMail API conventions: the base URL, /v0 versioning, authentication, requests and responses, pagination, idempotency, request IDs, CORS and scoped routes.

Source: https://machinemail.dev/docs/reference

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](https://machinemail.dev/docs/reference/agentmail-compatibility).

The [endpoint reference](https://machinemail.dev/docs/reference/api) is generated from the OpenAPI document, served at `GET /v0/openapi.json`.

## Base URL

```text
https://api.machinemail.dev
```

| Surface                    | URL                                                                                               |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| REST API                   | `https://api.machinemail.dev/v0/...`                                                              |
| Remote MCP server          | `https://mcp.machinemail.dev/mcp` ([MCP tools](https://machinemail.dev/docs/reference/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:

```bash
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](https://machinemail.dev/docs/reference/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](#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](https://machinemail.dev/docs/reference/errors).

## Pagination

List endpoints take `limit` (default 50, max 500), `page_token` and `ascending`, and return `count`, `limit` and `next_page_token`. See [Pagination](https://machinemail.dev/docs/reference/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.

## Next

- [Endpoints](https://machinemail.dev/docs/reference/api): Every endpoint, request and response field
- [Authentication](https://machinemail.dev/docs/reference/authentication): Keys, scopes and permissions
- [Errors](https://machinemail.dev/docs/reference/errors): The error envelope and every error type
- [Webhook events](https://machinemail.dev/docs/reference/webhook-events): Event types, payloads and signatures
