---
title: "Switching from AgentMail"
description: "MachineMail serves AgentMail's /v0 API. Change the base URL and API key, keep your code and SDKs, and gain trust reports, catch-all and ephemeral inboxes."
canonical: "https://machinemail.dev/docs/getting-started/from-agentmail"
---

# Switching from AgentMail

> MachineMail serves AgentMail's /v0 API. Change the base URL and API key, keep your code and SDKs, and gain trust reports, catch-all and ephemeral inboxes.

Source: https://machinemail.dev/docs/getting-started/from-agentmail

MachineMail implements AgentMail's `/v0` API. Wherever AgentMail defines a path, a field name or an enum value, MachineMail uses the same one. Switching is a base URL and an API key. Still deciding? [MachineMail vs AgentMail](https://machinemail.dev/compare/agentmail) sets the two side by side, with a source for every claim.

## The change

### Get a MachineMail key

Sign up at [machinemail.dev/signup](https://machinemail.dev/signup) and copy the API key the dashboard shows after your first sign-in. Keys start with `mm_live_`. See the [Quickstart](https://machinemail.dev/docs/getting-started/quickstart).

### Point your client at MachineMail

**TypeScript SDK**

```ts title="mail.ts"
import { AgentMailClient } from 'agentmail';

const mail = new AgentMailClient({
  apiKey: process.env.MACHINEMAIL_API_KEY,
  baseUrl: 'https://api.machinemail.dev', // the only change
});

const inbox = await mail.inboxes.create({ username: 'support-agent', displayName: 'Support agent' });
await mail.inboxes.messages.send(inbox.inboxId, {
  to: ['ada@example.com'],
  subject: 'Hello',
  text: 'Sent through the AgentMail SDK, served by MachineMail.',
});
```

**Python SDK**

```python title="mail.py"
import os
from agentmail import AgentMail

mail = AgentMail(
    api_key=os.environ["MACHINEMAIL_API_KEY"],
    base_url="https://api.machinemail.dev",  # the only change
)

inbox = mail.inboxes.create(username="support-agent", display_name="Support agent")
mail.inboxes.messages.send(
    inbox.inbox_id,
    to=["ada@example.com"],
    subject="Hello",
    text="Sent through the AgentMail SDK, served by MachineMail.",
)
```

**HTTP**

```bash title="Terminal"
# Before
curl https://api.agentmail.to/v0/inboxes -H "Authorization: Bearer $AGENTMAIL_API_KEY"

# After
curl https://api.machinemail.dev/v0/inboxes -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

### Update webhook verification

MachineMail signs webhooks with its own `X-MachineMail-Signature` header. If you verify AgentMail webhooks with Svix, replace that check. See [Verify webhook signatures](https://machinemail.dev/docs/guides/verify-webhooks).

### Update your MCP config

If your agent uses MCP, point it at `https://mcp.machinemail.dev/mcp` with the MachineMail key as a Bearer token. Tool names differ from AgentMail's MCP server. See [MCP server](https://machinemail.dev/docs/agents/mcp) and [MCP tools](https://machinemail.dev/docs/reference/mcp-tools).

> **Note:** MachineMail's test suite runs the official `agentmail` npm package against a real MachineMail server, covering inboxes, messages (send, reply, reply-all, forward, labels, delete), threads, drafts,
> webhooks and domains. The Python SDK calls the same paths, but it is not part of that suite.

## What carries over

| AgentMail feature                                                                                                                  | On MachineMail                                                |
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Inboxes: create, list, get, update, delete, search                                                                                 | Same paths. `inbox_id` is the email address, as on AgentMail. |
| Messages: list, get, send, reply, reply-all, forward, update labels, delete, raw, attachments, `batch-get`, `batch-update`, search | Same paths and fields.                                        |
| Threads, at `/v0/threads` and `/v0/inboxes/{inbox_id}/threads`                                                                     | Same.                                                         |
| Drafts, including `send_at` scheduling                                                                                             | Same.                                                         |
| Webhooks: create, list, get, update, delete                                                                                        | Same, with a different signature header.                      |
| Custom webhook headers: `headers` on create, `.../webhooks/{webhook_id}/headers`                                                   | Same. Values are write-only, as on AgentMail.                 |
| Domains: create, list, get, update, delete, verify, `zone-file`                                                                    | Same, plus MachineMail fields.                                |
| Lists at `/v0/lists/{direction}/{type}`, also under pods and inboxes                                                               | Same.                                                         |
| Pods, and pod-nested routes under `/v0/pods/{pod_id}/...`                                                                          | Same.                                                         |
| API keys with AgentMail permission names, at org, pod and inbox level                                                              | Same.                                                         |
| `GET /v0/organizations`, `GET /v0/auth/me`                                                                                         | Same, plus usage counters.                                    |
| Metrics: `/v0/metrics/events`, `/usage`, `/rates`                                                                                  | Same paths, with MachineMail event types.                     |
| WebSocket at `/v0/ws` with `subscribe` messages                                                                                    | Same protocol.                                                |
| `client_id` idempotency on creates                                                                                                 | Same.                                                         |

## What MachineMail adds

These are extra fields and endpoints. AgentMail clients ignore what they do not know, so they do not break anything.

- **`trust` on every inbound message**, plus `trust_level` on list items and `GET /v0/inboxes/{inbox_id}/messages/{message_id}/trust`. See [Trust](https://machinemail.dev/docs/trust).
- **A `QUARANTINE` label.** Mail that tries to instruct your agent, or looks like phishing, gets `QUARANTINE` instead of `INBOX` and a `message.received.quarantined` event. Lists hide it unless you pass `include_quarantine=true`.
- **Ephemeral inboxes** with `ttl_seconds`, `ephemeral` and `expires_at`, and an `inbox.expired` event when they are deleted.
- **Catch-all domains** with `catch_all`, `catch_all_ttl_seconds` and `inbound_enabled` on domains, plus DNS host detection and Domain Connect. See [Domains](https://machinemail.dev/docs/concepts/domains).
- **Webhook delivery log**: `GET /v0/webhooks/{webhook_id}/deliveries`, replay and test.
- **An event log and SSE stream**: `GET /v0/events` and `GET /v0/events/stream`.
- **Suppressions** at `/v0/suppressions`, filled automatically from bounces and complaints.
- **Export** at `GET /v0/inboxes/{inbox_id}/export?format=mbox|jsonl`.
- **Agent self-registration** through [auth.md](https://machinemail.dev/docs/agents/auth-md).

In the TypeScript SDK, extension fields come through untouched and in snake\_case, so read them with a cast:

```ts title="trust.ts"
const message = await mail.inboxes.messages.get(inboxId, messageId);
const trust = (message as any).trust; // { level, score, content: { text }, reasons, ... }
if (trust?.level === 'quarantined') return;
```

## What is different

| Area                      | Difference                                                                                                                                                                                                                                                                |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Error bodies              | `{"error": {"type", "message", "param"}}` with the matching HTTP status. `type` is one of `not_found`, `validation`, `unauthorized`, `forbidden`, `rate_limited`, `conflict`, `internal`, `not_implemented`. See [Errors](https://machinemail.dev/docs/reference/errors). |
| Webhook signatures        | `X-MachineMail-Signature: t=<unix>,v1=<hex>`, an HMAC-SHA256 of `<t>.<body>` with the webhook `secret`.                                                                                                                                                                   |
| Webhook and stream bodies | AgentMail's shape (`type: "event"`, `event_type`, `event_id`, and the `message`, `thread` or `send` objects at the top level) plus a `data` object holding the same payload and a `timestamp`.                                                                            |
| Event types               | Adds `message.received.quarantined`, `inbox.created`, `inbox.expired`, `draft.sent` and `webhook.test`. There is no `message.opened` and there are no calendar events.                                                                                                    |
| Hidden labels             | List calls hide `SPAM`, `BLOCKED`, `TRASH`, `UNAUTHENTICATED` and `QUARANTINE` unless you pass the matching `include_*` flag.                                                                                                                                             |
| Permissions               | AgentMail's permission names, plus `inbound_raw` for `POST /inbound/raw`.                                                                                                                                                                                                 |
| Agent sign-up             | `/v0/agent/sign-up`, `/v0/agent/human` and `/v0/agent/verify` are not available. Agents register through `/auth.md` instead.                                                                                                                                              |
| Inbox event log           | `GET /v0/inboxes/{inbox_id}/events` returns MachineMail's event log for that inbox, not AgentMail's label-event log.                                                                                                                                                      |

## Not available yet

These AgentMail endpoints return `404` on MachineMail:

- Apps and accounts (`/v0/apps`, `/v0/accounts` and the inbox and pod variants)
- Calendar (`/v0/inboxes/{inbox_id}/calendar/...`)
- Hosted domain setup links (`/v0/domains/{domain_id}/setup-link`)
- `POST /v0/inboxes/{inbox_id}/authorize`
- Attachment downloads through threads and drafts (`/threads/{thread_id}/attachments/{attachment_id}`, `/drafts/{draft_id}/attachments/{attachment_id}`). Fetch attachments through the message instead.

The [AgentMail compatibility reference](https://machinemail.dev/docs/reference/agentmail-compatibility) lists every operation.

## Import your settings

The [MachineMail CLI](https://machinemail.dev/docs/cli) copies your AgentMail settings into your MachineMail organization: pods, custom domains, inboxes, webhooks, and allow and block lists. It never copies mail: no threads, messages, drafts or attachments.

The import runs on your machine. The CLI reads AgentMail with your own AgentMail API key and writes to MachineMail through the public API with your MachineMail key. The AgentMail key is sent only to AgentMail, never to MachineMail, and nothing at AgentMail is changed: the importer only reads.

### Sign in to MachineMail

Install the CLI and run `machinemail login`. Use an organization key, or a pod key to import into one pod. See [Installation](https://machinemail.dev/docs/cli/installation).

### Give the CLI your AgentMail key

Create a key in the AgentMail console. It only needs the read permissions `inbox_read`, `pod_read`, `domain_read`, `webhook_read`, `list_entry_read` and `api_key_read`; give it a short expiry. Then either export it as `AGENTMAIL_API_KEY`, keep it in a file for `--key-file`, or paste it when the CLI asks. The CLI never takes the key as an argument, so it stays out of your shell history, and shows it only masked (`am_us_…a91f`).

The region comes from the key prefix: `am_us_` reads `api.agentmail.to`, `am_eu_` reads `api.agentmail.eu`. Pass `--region us` or `--region eu` to choose it yourself.

A pod-scoped AgentMail key imports only that pod, and an inbox-scoped key only that inbox. Organization-level settings need an organization key.

### Preview the plan

```bash title="Terminal"
machinemail import agentmail --dry-run
```

The CLI shows what the key can see, then a plan: what it will create, what is already there, what it skips and why. `--dry-run` writes nothing and saves no file.

### Import

```bash title="Terminal"
machinemail import agentmail
```

The CLI asks before it creates anything (`--yes` skips the question). Then it writes an ID map, `agentmail-import-map.json` and `agentmail-import-map.csv` (`--map-file` changes the path), with each AgentMail id and its MachineMail id: pods, domains, inboxes (old address to new address) and webhooks. Last, it prints the cutover checklist below.

Running it again is safe. Every object is created with a `client_id`: the AgentMail object's own, or `am:<agentmail id>`. A second run reports everything as already imported and creates nothing; after a partial failure it creates what is missing. An inbox address that already exists in your organization, and was not created by an import, is never taken over: rename the AgentMail inbox with `--rename` or leave it out with `--skip`.

The command exits 0 when everything was imported, 1 when plan limits stopped it before anything was written or when some items failed (both are listed), 2 for a usage error, 3 when a key is refused or cannot do the import (for example a pod-scoped MachineMail key with an organization AgentMail key), 5 or 6 when AgentMail kept rate limiting or could not be reached, and 130 when interrupted.

### What is imported

| AgentMail         | On MachineMail                                                                                                                                                                                                                                            |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pods              | Name and `client_id`. AgentMail's Default Pod maps to your default pod.                                                                                                                                                                                   |
| Custom domains    | Added to MachineMail with new DNS records for you to publish. The CLI does not change your DNS.                                                                                                                                                           |
| Inboxes           | Display name, metadata, `client_id`, status (a paused inbox stays paused) and pod. Inboxes on your own domain keep their address.                                                                                                                         |
| `@agentmail.to`   | These addresses belong to AgentMail and cannot move. The CLI takes the same name on `machinemail.dev`. When that name is taken or reserved, it proposes one with a suffix; choose your own with `--rename old=new`, or leave the inbox out with `--skip`. |
| Allow/block lists | Every entry at the organization, pod and inbox level, in the same direction and list type. Read-only entries that AgentMail manages are not imported; the report lists them.                                                                              |
| Webhooks          | URL, event types, pod and inbox filters (pointing at the imported pods and inboxes) and `client_id`. Always created **disabled**, with a new signing secret. A webhook filtered on both pods and inboxes becomes two webhooks (see below).                |

Imported inboxes do not notify your webhooks: the CLI creates them with [`import_source: "agentmail"`](https://machinemail.dev/docs/reference/api/inboxes), so their `inbox.created` events are recorded in the event log (with `import_source` in the payload) but never sent to webhooks, which are not told about inboxes that already existed.

An AgentMail webhook filtered on both pods and inboxes receives events that match any of them. A MachineMail webhook with both filters needs an event to match both, so the CLI imports such a webhook as two: one for its pods and one for its inboxes outside those pods. Each has its own signing secret.

Webhook event types map one to one, except these, which MachineMail does not emit and the CLI drops (a webhook left with no event types is not created):

| AgentMail event type | On MachineMail                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `message.security.*` | Not emitted. The trust layer replaces Agent Armor; `message.received.quarantined` is the nearest. |
| `message.opened`     | Not emitted: MachineMail does not track opens.                                                    |
| `calendar.event.*`   | Not emitted: MachineMail has no calendar.                                                         |

### What is not imported

- **Mail.** No threads, messages, drafts or attachments. Keep the AgentMail account readable until in-flight threads are finished.
- **API keys.** The report lists each AgentMail key's name, scope and permissions so you can create MachineMail keys to match.
- **Webhook secrets and custom header values.** Each webhook gets a new secret. AgentMail never returns header values, so they cannot be copied: the report lists each webhook's header names with the command that sets them, for example `machinemail webhooks update <webhook-id> --header "Authorization: env:ROUTINE_TOKEN"`.
- **Polling webhooks.** Use a webhook or the event stream (`machinemail tail`).
- **Settings with no equivalent**, such as Agent Armor mode, open tracking and subdomain inboxes. The report names each one.

### Lists behave a little differently

The CLI warns, per level, where it matters, and names each block entry that an allow entry overrides:

- At AgentMail the narrowest level with a match decides. On MachineMail, organization, pod and inbox entries combine; an allow match wins over a block match, and any allow entry that applies makes that direction allow-only. An organization allow for `acme.com` therefore lets `bad@acme.com` through even when an inbox blocks it.
- MachineMail applies at most 500 entries per direction (send, receive, reply) in an organization, all levels and both list types together. The CLI warns when an import goes past that; trim the lists first.
- On MachineMail, receive lists also apply to replies. AgentMail checks replies only against reply lists. MachineMail stores reply lists but does not apply them to mail yet.
- Mail from a blocked sender is stored with the `BLOCKED` label and withheld from agents, rather than rejected.

### Plan limits

The CLI checks your plan before it writes anything, and stops with a message, creating nothing, when the import does not fit: more custom domains than the Free plan includes, or more than 1,000 inboxes on `machinemail.dev` for the organization. On a paid plan, domains beyond those included are billed; the plan says so before you confirm.

### Cut over

1. **Each custom domain**, in this order. MachineMail does not change your DNS.
   1. Publish every record the CLI printed under **Publish now**: DKIM, MAIL FROM and SPF, and DMARC. These do not move mail. If the domain already has an SPF record, add MachineMail's `include:` to it rather than publishing a second one; if it already has a DMARC record, keep it.
   2. Lower the MX record's TTL (for example to 300 seconds) and wait for the old TTL to pass.
   3. Cut over: replace AgentMail's MX record with MachineMail's in one change. Do not publish both: mail would split between the two providers. &#x2A;*Until MX changes, mail for the domain keeps going to AgentMail.**
   4. Wait until the domain is `VERIFIED` (`machinemail domains get <domain-id>`). While inbound mail is on, verification needs the new MX, so this comes last.
   5. Keep the AgentMail account for a few days, in case mail lands there while DNS catches up. See [Set up a custom domain](https://machinemail.dev/docs/guides/custom-domain).
2. **Stored ids:** pods, domains and webhooks have new MachineMail ids, and so do inboxes whose address changed. Update code and config that store a `pod_id`, `domain_id`, `webhook_id` or `inbox_id`; the ID map lists each old id and its new one. It also holds your webhook URLs, which can carry tokens, so keep it private. Tell your correspondents about new addresses: old `@agentmail.to` addresses keep receiving at AgentMail, and nothing forwards them.
3. **Webhooks:** update your receivers to check `X-MachineMail-Signature` instead of the Svix headers (see [Verify webhook signatures](https://machinemail.dev/docs/guides/verify-webhooks)). If a webhook had custom headers at AgentMail, set their values before you enable it: `machinemail webhooks update <webhook-id> --header "Name: -"` reads a value from stdin and `--header "Name: env:VAR"` from an environment variable (the checklist prints the command for each webhook; see [Custom headers](https://machinemail.dev/docs/concepts/webhooks#custom-headers)). Read each new secret with `machinemail webhooks get <webhook-id>`, then turn each webhook on with `machinemail webhooks update <webhook-id> --enabled` (the checklist prints one command per webhook). Leave off any the report says were disabled at AgentMail.
4. **Your code:** create MachineMail API keys, change the base URL as above, and check the operations you use against the [compatibility reference](https://machinemail.dev/docs/reference/agentmail-compatibility).
5. **Revoke the AgentMail key** you used for the import.

## Next

- [Trust](https://machinemail.dev/docs/trust): The part AgentMail does not have.
- [Inboxes](https://machinemail.dev/docs/concepts/inboxes): Named, catch-all and ephemeral inboxes.
- [Webhooks](https://machinemail.dev/docs/concepts/webhooks): Signing, retries and the delivery log.
