Inboxes · September 21, 2026
Agent Email on a Subdomain Without Breaking Company Mail
Run agent mail on agents.yourcompany.com: inspect existing MX, publish ownership, SPF, DKIM and receiving records, and leave employee routing untouched.
The failure mode this article exists to prevent takes ninety seconds to cause and a
working day to undo: someone pastes a set of MX records into the zone for
yourcompany.com because a setup wizard displayed them, and every employee’s mail
starts routing to a platform that has an inbox for exactly one address. Nothing bounces
loudly. Mail simply arrives somewhere nobody is reading it.
An email subdomain for an AI agent — agents.yourcompany.com — avoids that, because
the hostname you are changing has no mail on it to break. Below: what to check before
you touch DNS, what each record proves, why the default mode refuses to enable
receiving on a hostname that already carries mail, and how to read three readiness
states that are allowed to disagree.
One caveat before you plan around it: attaching a custom domain requires a live project
on an eligible paid subscription, and paid checkout is currently disabled. Builder and
Team are proposed plans that are not available to buy yet, and an attach without an
eligible plan returns HTTP 402 payment_required. The usable address today is a
platform address on agents.emailforagents.ai; pricing has the detail.
Why a subdomain and not the root domain
MX is per-hostname, and that single fact drives the design. example.com and
agents.example.com are different names with independent MX sets: publishing MX at the
first redirects every employee’s mail, publishing MX at the second touches a hostname
that five minutes ago received nothing.
Three consequences, in the order they will cost you something:
- Receiving is a replacement, not an addition. There is no “also deliver a copy here” in MX. The set at a hostname is the routing table for it, and readiness requires an exact match against the provider’s set — so you cannot keep your old records alongside as a hedge and still reach a receiving-ready state.
- Sending reputation stays separated. A complaint or bounce generated by an agent
lands against
agents.example.com, not the name your invoices go out from. - Rollback is a zone edit. Removing three records from a subdomain nobody depends on takes a minute. Restoring a root zone’s mail routing under pressure does not.
The preview step encodes this. When it resolves MX at the hostname you typed and finds
records, the returned plan marks the receiving row blocked_existing_mx, sets
overwrite_blocked: true, and populates recommended_subdomain with
agents.<what you typed>. The field takes a hostname and nothing else:
https://agents.example.com, ada@example.com, a bare IPv4 address and localhost
are all refused with HTTP 400 invalid_request.
Inspect what your DNS already does
Do this from a shell before you open the console, because it takes a minute and it tells you which of the two paths below you are on.
# Does the hostname you want already receive mail?
dig +short MX agents.example.com
dig +short MX example.com
# What SPF already exists, and at which name?
dig +short TXT example.com | grep -i spf1
dig +short TXT agents.example.com | grep -i spf1
# Nothing should be here yet.
dig +short TXT _efa-challenge.agents.example.com
| What you see | Path |
|---|---|
No MX at agents.example.com | Clean. Attach it, keep the default mode, publish everything |
MX at agents.example.com | Something already receives there. Pick another label, or read the migration section closely |
MX at example.com only | Normal company mail. Nothing to do — do not attach the root |
| An SPF record at the root | Normal. Leave it alone; the subdomain gets its own |
The platform’s own check is not dig. It is DNS-over-HTTPS against a fixed origin
(cloudflare-dns.com/dns-query, Accept: application/dns-json), redirects refused, a
seven-second deadline across the whole resolution, and at most one CNAME alias followed
per name — two aliases at one name is a hard error rather than a guess.
The part worth internalising: a resolver failure is never reported as “no MX found.”
Any failure becomes HTTP 503 temporarily_unavailable, with the message “DNS could not
be checked. Existing records have not been assumed absent.” A flaky lookup cannot talk
the system into believing your hostname is free.
POST /v1/projects/{project_id}/domains/preview does the same resolution and returns
the record plan without storing anything. Every response in this flow carries
mutates_customer_dns: false — the invariant the whole thing is built around. Nothing
writes, edits or removes a record in your zone. You publish; the system reads.
One environment rule saves a detour: a test project cannot attach a domain. POST /v1/projects/{project_id}/domains returns HTTP 422 test_mode_unsupported, and test
addresses stay on non-routable .mail.invalid hosts. Preview still works, returning a
fixture plan whose ownership token is the literal fixture-not-live.
Ownership first: the challenge TXT record
Attaching issues exactly one secret: a token of the form efa_ followed by 48 hex
characters. Only its SHA-256 is stored, so the plaintext is shown once, in the attach
response as ownership_token and in the console. Publish it as a TXT record:
Type TXT
Host _efa-challenge.agents.example.com (relative: _efa-challenge)
Value efa-ownership=efa_9c41a7...
Both host forms are shown because DNS providers disagree about which they want: some
expect the fully-qualified name, some expect _efa-challenge and append the zone.
Publishing both creates a record at
_efa-challenge.agents.example.com.agents.example.com, the most common reason
verification never completes — the record exists, just not at the name being queried, so
the check reports missing.
The comparison is exact string equality. The resolver reassembles a TXT record split
into quoted 255-byte chunks, so a re-wrapped long string is handled correctly; an added
pair of quotes, a stray space or a truncated paste produces mismatch. missing means
nothing was observed at that host; mismatch means something was, and it was not what
was expected. They point at different mistakes, which is why they are not merged.
At this stage the sending and receiving rows exist in the plan but hold no values —
status awaiting_provider. Mail-routing records are not handed out for a hostname
nobody has demonstrated control of.
Only one verification runs at a time per domain, held by a two-minute lease; a second
concurrent request returns HTTP 503 temporarily_unavailable rather than racing the
first. And the challenge TXT stays published permanently — it is re-read in the
background, not just once at setup.
Sending records: SPF and DKIM at the correct host
Once ownership verifies, the provider issues the sending records and they appear with
their type, host and value. Publish them verbatim; do not retype a DKIM key, copy it.
Provider record names arrive relative and are expanded against your hostname before
display, so @ or an empty name means agents.example.com itself.
The one mistake that causes real damage here is an SPF collision. SPF is per-hostname.
The record shown for agents.example.com belongs at agents.example.com. Pasting it
over the TXT record at example.com can stop your existing company mail authenticating,
and that failure is invisible until recipients start rejecting mail. A hostname should
also carry exactly one SPF record — two at one name is a permanent error in the
specification, not something a receiver merges for you.
sending_status reaches ready only when all of the following hold at once: the
provider reports the domain verified, the record set contains at least one SPF record
and at least one DKIM record, and every one of those records is verified. If any sending
record has failed, the status is failed. Anything else is pending. There is no
partial credit and no “mostly ready.”
Receiving records: MX value and priority
Receiving MX is issued on the same event — ownership verifying — but only if the
receiving capability was enabled when the provider domain was created. That decision is
made once, from two inputs: whether you chose migrate, and whether MX resolved at the
hostname at that moment. No existing MX, or explicit migration consent, enables
receiving; otherwise the domain is created with receiving off and no MX values are ever
issued.
Publish the issued records at the hostname with the exact priority shown, then re-check. Readiness compares the live MX set against the provider’s as a set: the counts must match, each live exchange must match a provider record’s value, each priority must match, and the provider record must target the hostname itself.
Anything else holds receiving_status at pending — including the case people find
most surprising, a leftover backup MX at priority 50 pointing at an old server. That is
not clutter. It is a real routing path elsewhere that any sender may use, so readiness
will not assert that mail for the hostname arrives here while it exists. Remove it.
Each row in the plan carries its own status while you work — pending, found,
missing, mismatch, awaiting_provider (the value does not exist yet and appears
after ownership verifies) and blocked_existing_mx (receiving here would replace
existing MX and you chose to preserve it). The
domains reference tabulates them.
Why “preserve existing MX” is the safe default
receiving_mode defaults to preserve_existing on POST /v1/projects/{project_id}/domains:
POST /v1/projects/{project_id}/domains
Content-Type: application/json
{
"domain": "agents.example.com",
"receiving_mode": "preserve_existing"
}
On a clean subdomain the default costs nothing: there is no MX to preserve, so the provider domain is created with receiving enabled and the flow proceeds normally. The default only bites on a hostname that already has mail, and there it does exactly what you want.
The mechanism is stronger than “we show a warning.” At attach, the live MX set is
snapshotted onto the domain row. If that snapshot is non-empty and the mode is
preserve_existing, then when ownership verifies receiving_status is set to
disabled — not pending. Disabled is terminal for that attachment: no MX values are
issued, and publishing MX yourself will not move it, because there is nothing to
compare against.
The consequence to plan for: on a hostname that already receives mail, the safe
default gives you sending verification only. Creating an inbox there is refused with
HTTP 422 domain_not_ready — “Ownership, sending, and receiving must all be verified
before creating an inbox.” Attach agents.example.com, which has no MX, and the same
default becomes invisible.
The migration option and exactly what it interrupts
Sending "receiving_mode": "migrate" records a timestamped consent on the domain row
and changes what the wizard offers. It changes nothing in your zone: the response still
says mutates_customer_dns: false, and the domain.updated event carries
migration_consent: true alongside that same field.
So the honest answer to “what does migrate interrupt” is: at the moment you choose it,
nothing. The interruption happens when you publish the MX records, and then it is total
for that hostname. Every address at it routes to the agent platform, and mail to a name
with no matching inbox has nowhere to land. Because readiness requires an exact set, you
cannot soften that by leaving the old MX at a higher priority number — do it and
receiving stays pending, giving you a half-migrated zone with none of the benefit.
Migrate is right in one situation: you control the hostname, you intend all its mail to be handled by agent inboxes, and you have enumerated the addresses receiving there now. If you cannot list them, you are not ready to choose it.
Three separate readiness states, checked separately
Ownership, sending and receiving are three columns, not three stages of one bar. They are allowed to disagree, and usually do for a while.
| Status | Values | Gates |
|---|---|---|
ownership_status | pending, verified, lost | Everything; losing it disables routing for every inbox on the domain |
sending_status | disabled, pending, ready, failed | Whether the provider has verified SPF and DKIM here |
receiving_status | disabled, pending, ready, failed | Whether mail to the hostname arrives here |
Every transition emits a domain.updated event carrying all three values plus
mutates_customer_dns: false, so you can follow setup from
the event stream rather than by refreshing a page.
One behaviour catches people who treat the check button as free. An explicit
POST /v1/projects/{project_id}/domains/{domain_id}/verify may ask the provider to
re-run its own verification — at most once a minute, and only when sending is not ready
or receiving is pending. When it fires, sending_status drops back to pending on
purpose, because the provider has restarted verification and the earlier result is no
longer current evidence, and routing for that domain’s inboxes is disabled until the
next successful check. Clicking in a tight loop keeps you in pending rather than
getting you to ready sooner. Check, wait for propagation, check again. Between your
checks DNS is re-read in the background, scheduled five minutes after the last provider
check.
Diagnosing send-ready-but-not-receive-ready
This is the most common place to get stuck, and the diagnosis branches on one question:
is receiving_status disabled or pending? They mean different things.
disabled means no MX values were issued — MX existed at attach time and you kept
preserve_existing, or the provider domain was created with receiving off. Publishing
records will not help; attach a dedicated subdomain instead.
pending means values were issued and what is published does not match yet:
- Count the records.
dig +short MX agents.example.comshould return exactly the number of rows the console shows. An extra line, including a backup, is the answer. - Compare priorities, not just hostnames. The same exchange at the wrong priority is a mismatch.
- Check for a doubled or relative host. The record belongs at the hostname itself,
not at
agents.example.com.agents.example.com. - Wait out negative caching. A resolver that recently answered NXDOMAIN keeps doing so for the zone’s negative TTL.
- Check whether you tripped a verification restart. Several checks in a minute may
have reset sending to
pendingunderneath you.
If inbox creation is what fails rather than a status, HTTP 422 domain_not_ready means
one of the three is not ready. The response does not say which; the domain record does.
Rolling back
The domain API surface is small on purpose: preview, attach, read, verify. There is no self-serve detach, so rollback is a DNS operation, and the order matters.
- Pause or delete the inboxes on that domain first, so no agent is mid-conversation at an address about to stop resolving.
- Remove the receiving MX records. Inbound mail stops arriving as resolvers age out the old answers.
- Remove the sending SPF and DKIM records if you are not coming back.
- Remove the challenge TXT last, and only if you are abandoning the hostname.
The challenge is the switch that turns the attachment off. The next check — yours, or
the background pass — finds it missing, moves ownership_status from verified to
lost, sets sending and receiving to disabled, flips every address on the domain to
disabled routing, and emits a domain.updated event recording it.
Two things rollback does not do. A later owner of the hostname does not inherit your
token, so buying a domain does not buy its previous owner’s verification. And if
provider attachment shows unknown or failed, do not delete and re-attach:
provider-side creation may have completed despite a timeout, and a second attempt can
leave two claims on one hostname. The response says as much — “This domain is reserved;
contact support before retrying attachment.”
Why a ready status is configuration evidence, not a delivery promise
Three green states is a precise claim, and narrower than it looks. It says the expected challenge string was observed in DNS, the provider reported SPF and DKIM verified for this hostname, and the MX set published there matched the provider’s set exactly at the last check. All three are statements about configuration.
None of them is about where your mail lands. Authentication is the entry fee for being
evaluated, not a verdict; no provider can promise inbox placement, and there is no
published uptime SLA or delivery guarantee here. Keep the outbound distinction sharp for
the same reason: a send returns HTTP 202 with state queued, message.accepted means
the provider acknowledged the submission, and only the later per-recipient
message.delivery_updated events say anything about a recipient’s server.
The API loop covers that chain.
So finish with the two-direction proof rather than a status page. Send from a mailbox
you control to the new address and confirm a message.received event; have the inbox
reply and follow it through queued and accepted to the delivery updates. Until mail
has moved both ways, you have verified your DNS, not your setup.
Next steps
- Record by record: custom domains.
- Starting on a platform address instead: give an AI agent its own email address.
- Whether a dedicated inbox is the right shape: agent inbox vs connecting Gmail.
- The wider picture: email for AI agents.
Keep reading
Email for AI Agents: How Agent Inboxes Actually Work
What an email inbox for an AI agent is, how mail arrives and leaves, which permissions matter, and when an agent needs its own address instead of yours.
How to Give an AI Agent Its Own Email Address
Create a dedicated inbox, choose read, draft or send permission, connect over MCP or a scoped key, and prove a real round trip before granting send access.
AI Agent Inbox vs Connecting Gmail: How to Choose
Giving an agent its own inbox versus wiring it into an existing Gmail or Microsoft 365 mailbox: identity, blast radius, OAuth scope and revocation.