from agentmail.md


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 sets the two side by side, with a source for every claim.

The change

Get a MachineMail key

Sign up at machinemail.dev/signup and copy the API key the dashboard shows after your first sign-in. Keys start with mm_live_. See the Quickstart.

Point your client at MachineMail

TypeScript SDK

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

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

# 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.

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 and 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.

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

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.
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:

The AgentMail compatibility reference lists every operation.

Import your settings

The MachineMail 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.

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

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

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", 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

Lists behave a little differently

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

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. *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.
  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). 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). 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.
  5. Revoke the AgentMail key you used for the import.

Next