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
agentmailnpm 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.
truston every inbound message, plustrust_levelon list items andGET /v0/inboxes/{inbox_id}/messages/{message_id}/trust. See Trust.- A
QUARANTINElabel. Mail that tries to instruct your agent, or looks like phishing, getsQUARANTINEinstead ofINBOXand amessage.received.quarantinedevent. Lists hide it unless you passinclude_quarantine=true. - Ephemeral inboxes with
ttl_seconds,ephemeralandexpires_at, and aninbox.expiredevent when they are deleted. - Catch-all domains with
catch_all,catch_all_ttl_secondsandinbound_enabledon domains, plus DNS host detection and Domain Connect. See Domains. - Webhook delivery log:
GET /v0/webhooks/{webhook_id}/deliveries, replay and test. - An event log and SSE stream:
GET /v0/eventsandGET /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.
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:
- Apps and accounts (
/v0/apps,/v0/accountsand 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 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
- 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.comtherefore letsbad@acme.comthrough 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
BLOCKEDlabel 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
- Each custom domain, in this order. MachineMail does not change your DNS.
- 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. - Lower the MX record's TTL (for example to 300 seconds) and wait for the old TTL to pass.
- 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.*
- 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. - Keep the AgentMail account for a few days, in case mail lands there while DNS catches up. See Set up a custom domain.
- 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
- 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_idorinbox_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.toaddresses keep receiving at AgentMail, and nothing forwards them. - Webhooks: update your receivers to check
X-MachineMail-Signatureinstead 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 withmachinemail webhooks get <webhook-id>, then turn each webhook on withmachinemail webhooks update <webhook-id> --enabled(the checklist prints one command per webhook). Leave off any the report says were disabled at AgentMail. - Your code: create MachineMail API keys, change the base URL as above, and check the operations you use against the compatibility reference.
- Revoke the AgentMail key you used for the import.