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:

  1. 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.
  2. 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.
  3. 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 seePath
No MX at agents.example.comClean. Attach it, keep the default mode, publish everything
MX at agents.example.comSomething already receives there. Pick another label, or read the migration section closely
MX at example.com onlyNormal company mail. Nothing to do — do not attach the root
An SPF record at the rootNormal. 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.

StatusValuesGates
ownership_statuspending, verified, lostEverything; losing it disables routing for every inbox on the domain
sending_statusdisabled, pending, ready, failedWhether the provider has verified SPF and DKIM here
receiving_statusdisabled, pending, ready, failedWhether 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:

  1. Count the records. dig +short MX agents.example.com should return exactly the number of rows the console shows. An extra line, including a backup, is the answer.
  2. Compare priorities, not just hostnames. The same exchange at the wrong priority is a mismatch.
  3. Check for a doubled or relative host. The record belongs at the hostname itself, not at agents.example.com.agents.example.com.
  4. Wait out negative caching. A resolver that recently answered NXDOMAIN keeps doing so for the zone’s negative TTL.
  5. Check whether you tripped a verification restart. Several checks in a minute may have reset sending to pending underneath 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.

  1. Pause or delete the inboxes on that domain first, so no agent is mid-conversation at an address about to stop resolving.
  2. Remove the receiving MX records. Inbound mail stops arriving as resolvers age out the old answers.
  3. Remove the sending SPF and DKIM records if you are not coming back.
  4. 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


Keep reading

All guides · Documentation · Inboxes