# SPF, DKIM and MX on a subdomain for agent email
URL: https://emailforagents.ai/docs/domains
Summary: Verify your domain with a TXT challenge, publish SPF, DKIM and MX on a dedicated subdomain, and run agent mail without touching your existing email routing.

1. [Home](https://emailforagents.ai/)
2. [Docs](https://emailforagents.ai/docs)
3. Custom domains

Docs

## Use your own domain

Sending agent mail from a domain you control means publishing DNS records yourself. This application never writes, edits or removes a record on your behalf — it tells you the exact values and then checks what is actually published.

Last reviewed September 21, 2026 against the
implementation in this repository.

A platform address on `agents.emailforagents.ai` works with no DNS at all and is the
right place to start; this page is for when an
[agent inbox](https://emailforagents.ai/product/inboxes) needs to be at your own domain instead. If you have
not created any inbox yet, do the [quickstart](https://emailforagents.ai/docs/quickstart) first.

### What you need first

- **A live project.** Test projects cannot attach domains at all; the attempt returns `test_mode_unsupported`, and test addresses stay on non-routable `.mail.invalid` hosts.
- **An eligible paid subscription.** Attaching a domain without one returns `payment_required`. Each plan has a custom-domain count; exceeding it returns `quota_exceeded`.
- **Access to your DNS zone**, and a change window if the domain already carries mail.

**Paid checkout is currently disabled**, so the paid subscription this feature
requires cannot be bought today. Domain setup does not route around that requirement. Until
checkout ships, the usable path is a platform address. See [pricing](https://emailforagents.ai/pricing).

### Start with a dedicated subdomain

Use a hostname such as `agents.example.com` rather than your company's root domain.
Three reasons, in order of how much they will cost you if you ignore them:

1. **Receiving mail at a hostname means publishing MX there.** Publish MX at a
hostname that already receives employee mail and you redirect that mail. A subdomain has no
existing MX to break.

2. **Sending reputation stays separated.** An agent that gets a bounce or a
complaint does not drag your primary domain's reputation with it.

3. **Revocation is a zone edit.** Removing a subdomain's records is a contained
change you can make in a minute.

Preview the exact hostname in Domains before attaching it. The preview resolves DNS to see
whether MX already exists; it does not attach a provider domain and it changes nothing. If MX
is found, keep the default `preserve_existing` mode: that enables sending
verification for the hostname and leaves receiving disabled, so nothing you publish can
interrupt delivery to existing mailboxes. The preview will also suggest
`agents.` as the safer target.

Choose `migrate` only when you genuinely intend to move all receiving mail
at that hostname. It records your consent; it still makes no DNS changes.

### The records, and the order they appear in

You do not get every record up front, and that is deliberate: the provider-issued sending and
receiving values are only created after ownership is proven. Asking you to publish them earlier
would mean handing out mail-routing records for a hostname nobody had demonstrated control of.

| Stage | Record | Purpose | Where it comes from
| 1 | `TXT` at `_efa-challenge.` | Ownership |
Issued immediately on attach. Value is `efa-ownership=` plus a token unique to
this attachment.

| 2 | `TXT` at the domain — SPF | Sending | Issued by the mail provider after ownership verifies.
| 2 | DKIM record at the host the provider names | Sending |
Issued by the provider after ownership verifies. Publish the type, host and value exactly
as displayed; do not retype the key.

| 3 | `MX` at the domain, with a priority | Receiving |
Issued by the provider after ownership verifies, and only when receiving is not held back
by existing MX.

```
Type   TXT
Host   _efa-challenge.agents.example.com     (relative: _efa-challenge)
Value  efa-ownership=efa_9c41a7...
```

The console shows both the fully-qualified host and the relative host, because DNS providers
disagree about which one they want — some expect
`_efa-challenge.agents.example.com`, others expect `_efa-challenge` with
the zone appended for you. Publishing both forms creates a record at
`_efa-challenge.agents.example.com.agents.example.com`, which is the single most
common reason verification does not complete.

Each record carries its own status while you work:

| Record status | Meaning
| `pending` | Published value expected; not yet observed.
| `found` | The expected value was observed in DNS.
| `missing` | Nothing at all was found at that host.
| `mismatch` | Something is there, but it is not the expected value. Usually a truncated or re-wrapped TXT string.
| `awaiting_provider` | The value does not exist yet. It appears after ownership verifies.
| `blocked_existing_mx` | Receiving here would replace existing MX, and you chose to preserve it.

**Do not replace an existing root SPF record with a value intended for a sending subdomain.** SPF is per-hostname: the record shown for
`agents.example.com` belongs at `agents.example.com`. Overwriting the
SPF at `example.com` with it can stop your existing mail authenticating. A
hostname should also have exactly one SPF TXT record — two is a permanent error in the
specification, not a merge.

### Verifying ownership

1. Attach the domain. Copy its unique `_efa-challenge` TXT record into your DNS provider.
2. Leave the TXT record in place — permanently — and choose **Check verification and provider setup**.
3.
Once ownership is confirmed, the console displays the exact SPF, DKIM and receiving records
returned by the mail provider, including MX priority.

4. Publish those records at their displayed hosts.
5. Check again after DNS propagates. Each check reads DNS live; it never writes.

Only one verification check runs at a time per domain — a second concurrent request returns
`temporarily_unavailable` rather than racing the first. An explicit check can also
ask the provider to re-run its own verification, at most once a minute. Between your checks,
the system re-reads DNS in the background roughly every five minutes, so a record you remove
later will be noticed.

### Three statuses, not one

Ownership, sending and receiving are tracked separately because they can genuinely disagree: a
domain can be authenticated for sending while receiving nothing.

| Status | Values | What it gates
| `ownership_status` | `pending`, `verified`, `lost` | Everything. Losing it disables routing for every inbox on the domain.
| `sending_status` | `disabled`, `pending`, `ready` | Whether the provider has verified SPF and DKIM for the hostname.
| `receiving_status` | `disabled`, `pending`, `ready` | Whether mail addressed to the hostname arrives here.

Receiving readiness requires the MX set at the hostname to match the provider's set
exactly — same hosts, same values, same priorities, and no extras. An additional MX at
any priority is a real routing path to somewhere else, so readiness stays
`pending` until it is removed. This is the check that most often surprises people
who left an old record behind.

### Creating an address

Attaching a domain does not create an inbox. When the statuses you need are ready, go back to
Inboxes, select the same live project, choose the custom hostname under
**Address domain**, and give the inbox a display name and optional local part.

Then prove both directions before you hand it to an agent: send a message to the new address
from a mailbox you control, and have the inbox reply. A ready status is evidence that the
configuration is correct. It is not a promise about whether a particular recipient's provider
will place your mail in their inbox, and nothing on this page can be.

### Using the API

```
POST /v1/projects/{project_id}/domains
{
  "domain": "agents.example.com",
  "receiving_mode": "preserve_existing"
}
```

`receiving_mode` defaults to `preserve_existing`. Supply
`"receiving_mode": "migrate"` to record an intentional migration of all mail at that
hostname; it changes what the wizard will offer, not your DNS.

| Call | Does
| `POST /v1/projects/{project_id}/domains/preview` | Resolves DNS and returns the record plan. Nothing is stored.
| `POST /v1/projects/{project_id}/domains` | Attaches the domain and issues the ownership token.
| `GET /v1/projects/{project_id}/domains/{domain_id}` | Returns every record with its host, expected value, priority and status.
| `POST /v1/projects/{project_id}/domains/{domain_id}/verify` | Re-reads DNS and re-checks provider setup.
| `POST /v1/projects/{project_id}/inboxes` | With `domain_id` and an optional `local_part`, creates the address.

`The domain must be a hostname, not a URL or a mailbox: agents.example.com`, never
`https://agents.example.com` or `ada@example.com`. Bare IP addresses and
`localhost` are refused. Anything else returns `invalid_request` with a
message saying so. Every response carries `mutates_customer_dns: false`, which is
not decoration — it is the invariant this whole flow is built around.

### When setup needs attention

| Symptom | What to do
| **Ownership lost** |
The challenge TXT is gone or changed. Restore the exact value. Sending and routing stop
until verification succeeds again, and a later owner of the domain gets a new token
rather than inheriting yours.

| **Ownership record shows `mismatch`** |
Something is published but not the expected string. Check for added quotes, a split TXT
string, or the doubled-host mistake above.

| **Receiving stuck at `pending`** |
Compare host, value and priority exactly, and remove every MX that is not in the
provider's set. An extra record keeps receiving pending.

| **Sending stuck at `pending`** |
The provider has not yet verified SPF and DKIM. Confirm both are at the hostname the
console names, then re-check. DNS caches can hold an old negative answer for a while.

| **Provider status unknown or failed** |
Do not repeatedly delete and re-attach. Provider-side creation may have completed despite
a timeout, and re-attaching can leave two claims on one hostname. Custody in that state
needs support review.

| **Inbox paused right after creation** |
The address is reserved while a provisioning result is reconciled. Creating it again will
not silently allocate a second address.

Related: [restrict who the new address may write to before you use it](https://emailforagents.ai/docs/sending-controls), [the domain.updated event](https://emailforagents.ai/docs/events-and-webhooks#catalogue) that
fires when a status changes, and [finding mail once it arrives](https://emailforagents.ai/docs/search).

Longer form: [a dedicated agent inbox compared with connecting an existing mailbox](https://emailforagents.ai/blog/ai-agent-inbox-vs-gmail).

[Previous Events & webhooks](https://emailforagents.ai/docs/events-and-webhooks) [Next Attachments](https://emailforagents.ai/docs/attachments)
