# Custom domains (/advanced/custom-domains)

<!-- agent-signals: reading_time_min: 9 · est_tokens: 3658 · updated: 2026-09-06 -->
Related: [Webhooks](/advanced/webhooks.md), [WebSockets](/advanced/websockets.md), [Agent safety](/advanced/safety.md), [Deliverability and warmup](/advanced/deliverability.md), [Build a multi-tenant platform](/advanced/multi-tenant.md), [AgentID public-key authentication](/advanced/agentid.md)



# Register and verify a custom domain

Registering a domain returns the exact DNS records to publish, and verification makes the domain ready for inboxes, so agents send and receive as `support@example.com` under your brand and your own sending reputation. Use it on the Developer plan and above, on a root domain or on a dedicated subdomain that runs AgentMail alongside an existing mail provider.

## Do this

Register the domain. The response is the domain object: `status` starts at `NOT_STARTED`, `records` lists every DNS record to publish, and `domain_id` (the domain name itself) identifies the domain in every later call.

```bash
curl -X POST "https://api.agentmail.to/v0/domains" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "example.com" }'
```

Publish every record in the returned `records` array at the DNS provider that controls the domain's authoritative nameservers. Where the provider supports imports, download the records as a BIND zone file instead of entering them one by one:

```bash
curl "https://api.agentmail.to/v0/domains/example.com/zone-file" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" -o example.com.zone
```

Request verification (returns `204` with no body), then get the domain to watch progress. The get runs a live DNS check and returns the overall `status` plus a `status` per record.

```bash
curl -X POST "https://api.agentmail.to/v0/domains/example.com/verify" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"

curl "https://api.agentmail.to/v0/domains/example.com" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

Once `status` reads `VERIFIED`, create inboxes on the domain:

```bash
curl -X POST "https://api.agentmail.to/v0/inboxes" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "username": "support", "domain": "example.com", "display_name": "Support" }'
```

## SDK

Install: `npm install agentmail` (TypeScript), `pip install agentmail` (Python), `npm install -g agentmail-cli` (CLI).

* Register: `client.domains.create({ domain: "example.com" })` (TS), `client.domains.create(domain="example.com")` (Python), `agentmail domains create --domain "example.com"` (CLI).
* Get: `client.domains.get("example.com")`, `client.domains.get(domain_id="example.com")`, `agentmail domains get --domain-id "example.com"`.
* Verify: `client.domains.verify("example.com")`, `client.domains.verify(domain_id="example.com")`, `agentmail domains verify --domain-id "example.com"`.
* Zone file: `client.domains.getZoneFile("example.com")`, `client.domains.get_zone_file(domain_id="example.com")`, `agentmail domains get-zone-file --domain-id "example.com"`.
* Update: `client.domains.update("example.com", { subdomainsEnabled: true })`, `client.domains.update(domain_id="example.com", subdomains_enabled=True)`, `agentmail domains update --domain-id "example.com" --subdomains-enabled=true`.
* Delete: `client.domains.delete("example.com")`, `client.domains.delete(domain_id="example.com")`, `agentmail domains delete --domain-id "example.com"`.
* List: `client.domains.list()`, `agentmail domains list`.
* Pod-owned domains: `client.pods.domains` in the SDKs, `pods:domains` in the CLI.

Client setup is on [/integrations/sdks-and-cli](/integrations/sdks-and-cli).

## Facts

* `POST /v0/domains` params: `domain` (required, needs at least one dot), `feedback_enabled` (default `true`, delivers bounce and complaint notifications to your inboxes as emails), `subdomains_enabled` (default `false`), `tracking_enabled` (default `false`), `client_id` (idempotency id, retrying with the same value returns the already registered domain instead of an error).
* Default records returned by `POST /v0/domains`: DKIM `TXT` on `agentmail._domainkey` with a per-domain value starting `v=DKIM1; k=rsa; p=`, `MX` on `@` with value `inbound-smtp.us-east-1.amazonaws.com` priority `10`, `MX` on `mail` with value `feedback-smtp.us-east-1.amazonses.com` priority `10`, SPF `TXT` on `mail` with value `v=spf1 include:amazonses.com -all`, DMARC `TXT` on `_dmarc` with a value like `v=DMARC1; p=reject; rua=mailto:dmarc@example.com`.
* The DKIM record carries only the public half of the signing key. AgentMail holds the private key and signs outgoing mail with it.
* `subdomains_enabled` adds one required wildcard `MX` on host `*` (for a registered subdomain `mail.example.com`, the host is `*.mail`). `tracking_enabled` adds one required `CNAME` on host `link`, which must be published and verified before `track_opens` works on a send.
* Record names come back relative to the apex zone, with `@` for the apex. If the DNS editor shows fully qualified names, append the domain to the host.
* A host can carry only one SPF record. Merge `include:amazonses.com` into an existing SPF record right before its trailing `~all` or `-all`. Two SPF records on one host fail authentication.
* The `mail` host (bounce return path) must carry exactly one `MX` record.
* Domain `status` values and next action: `NOT_STARTED` (call verify), `PENDING` (required records not found yet, wait for propagation), `INVALID` (a record was found but is misconfigured, fix the value and MX priority of each record whose own `status` is `INVALID`), `FAILED` (records are in place, call verify again), `VERIFYING` (records correct, get again until it changes), `VERIFIED` (create inboxes).
* Per-record `status` values: `MISSING` (not found in DNS), `INVALID` (found with the wrong value), `VALID`.
* `POST /v0/domains/{domain_id}/verify` returns `204` with no body. AgentMail then rechecks on its own every few minutes while the domain is unverified, and about once a day once verified. The `domain.verified` webhook event fires when verification completes.
* DNS propagation takes from a minute to 48 hours depending on the provider, so a fresh record can read `MISSING` without anything being wrong. A stalled propagation or a `FAILED` status unsticks with another verify call.
* A `VERIFIED` domain whose records are later edited or deleted reverts, which blocks new inbox creation on it. Catch drift by getting the domain periodically and alerting when any record's `status` leaves `VALID`.
* Custom domains are available on the Developer plan and above. Mail from shared `@agentmail.to` addresses carries a `Sent via AgentMail` footer on the Free and Agent plans, mail from an inbox on your own verified domain does not.
* Domains are organization-level, not inbox-scoped. Register and verify once, then create any number of inboxes. Pod-owned domains use `/v0/pods/{pod_id}/domains`.
* `PATCH /v0/domains/{domain_id}` takes at least one of `feedback_enabled`, `subdomains_enabled`, `tracking_enabled`. Omitted fields stay unchanged. Enabling `subdomains_enabled` or `tracking_enabled` on a `VERIFIED` domain returns it to `PENDING` until the new record verifies. Existing inboxes keep sending and receiving during that window, new inbox creation waits for `VERIFIED`. Toggling `feedback_enabled` takes effect immediately with no DNS change.
* `DELETE /v0/domains/{domain_id}` returns `204`, is permanent, stops the domain's inboxes sending and receiving immediately, keeps stored threads and messages readable, frees a slot under the plan's domain cap, and lets the name be registered again.
* `GET /v0/domains` lists registered domains newest first. Optional `limit`, `page_token` (pass the previous response's `next_page_token`), and `ascending` (flips to oldest first). Entries carry settings and timestamps, get a single domain for `status` and a live record check.
* Inboxes on arbitrary subdomains (enabled by `subdomains_enabled`) send under the parent domain's DKIM identity and share its reputation, and they do not appear in `GET /v0/domains`. A separately registered subdomain gets its own DKIM identity and its own reputation.
* `dig +short MX example.com` shows where a name's mail goes: `10 inbound-smtp.us-east-1.amazonaws.com.` is AgentMail, `smtp.google.com` or `aspmx.l.google.com` is Google, a host ending in `protection.outlook.com` is Microsoft 365. An empty answer means no server receives the name's mail. Query each subdomain separately, MX records are independent per name.
* The DMARC policy can be softened to `p=quarantine` or `p=none` by editing the `_dmarc` TXT value at the DNS provider. Verification keeps passing while the value starts with `v=DMARC1` and keeps a `rua=mailto:` reporting address.
* Provider quirks that break verification: editors that append the domain to the host automatically (enter only the relative host or you get `agentmail._domainkey.example.com.example.com`), editors that auto-quote TXT values (paste values unquoted), Route 53's 255-character cap per TXT string (split the DKIM value into two quoted strings with nothing between them, `"first-half""second-half"`), records added in a registrar panel while the nameservers point elsewhere (only the provider serving the active nameservers counts), and stale or conflicting records on the same host (keep the returned value, remove duplicates).
* Google Workspace can deliver a subdomain's mail internally without consulting the subdomain's MX records, so mail from your own Workspace users never reaches AgentMail while outside mail arrives normally. Fix in the Google Admin Console under Apps, Google Workspace, Gmail, Routing with a rule that routes the subdomain's mail to its external MX records.
* If verification still fails, email `support@agentmail.cc` with the domain name and a screenshot of the DNS records.

## Not supported

* A domain whose mail already routes to Google Workspace or Microsoft 365 cannot be registered (`422 unprocessable`). Register a dedicated subdomain such as `agents.example.com` instead, the create checks the MX records of the exact name you register, not the root's.
* `agentmail.to` and every name under it is reserved and cannot be registered.
* Creating an inbox on a subdomain of a registered domain fails with a `422` unless `subdomains_enabled` is on. Registering a domain does not cover its subdomains by default.
* The verify call returns no verification result. Read progress with `GET /v0/domains/{domain_id}` or the `domain.verified` webhook event.
* Publishing DKIM, SPF, and DMARC records does not move inbound mail. Only MX records route mail, so pointing a root's MX at AgentMail stops the existing provider receiving every address on that root once the change propagates.
* Do not copy DNS record values from documentation samples. The DKIM value is generated per domain, only the returned `records` array is the source of truth.
* A zone file import replaces every existing record on some providers (Porkbun, for one). Do not import into a zone with records to keep, add the returned records manually instead.

## Errors

| Error                                    | Status | Cause                                                                                                                         | Fix                                                                                            |
| ---------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `validation_error` (`ValidationError`)   | 400    | `domain` on `POST /v0/domains` is malformed                                                                                   | the `errors` array shows the expected format                                                   |
| `already_exists`                         | 403    | domain create: your own organization already registered the name. Inbox create: the username is taken on the domain           | fetch the existing domain, or pick a username from the up to 3 alternatives in `suggestions`   |
| `resource_taken`                         | 403    | a different organization registered the domain                                                                                | pick a different name                                                                          |
| `limit_exceeded`                         | 403    | the plan's domain cap is reached                                                                                              | the error names the plan that raises the cap, deleting a domain frees a slot                   |
| `unprocessable`                          | 422    | domain create: the domain's mail already routes to Google Workspace or Microsoft 365, or the name is reserved                 | register a dedicated subdomain, or pick a different name                                       |
| `domain_not_verified`                    | 403    | inbox create before the domain reaches `VERIFIED`                                                                             | finish verification, the error names the domain                                                |
| `not_found` (message `Domain not found`) | 404    | inbox create on a domain not registered under your organization, or outside the key's scope                                   | check the registration and the key's scope                                                     |
| `422`                                    | 422    | inbox create on a subdomain without `subdomains_enabled`, or a zone file request for a domain that predates zone file support | enable subdomains with `PATCH /v0/domains/{domain_id}`, or publish the `records` array by hand |
| `400`                                    | 400    | `PATCH /v0/domains/{domain_id}` with an empty body                                                                            | send at least one of the three settings fields                                                 |

## Verify

```bash
curl "https://api.agentmail.to/v0/domains/example.com" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

Success is a domain object whose `status` is `VERIFIED` and whose every record `status` is `VALID`. Any code that decides whether the domain is usable reads this same `status` field.

## Related

* [/advanced/deliverability](/advanced/deliverability) for warming a new domain before it carries volume.
* [/advanced/multi-tenant](/advanced/multi-tenant) for pods that own domains and isolate each customer's resources.
* [/advanced/webhooks](/advanced/webhooks) for subscribing to the `domain.verified` event.
* [/core/send](/core/send) and [/core/receive](/core/receive) for using inboxes created on the domain.
