---
title: "Domains"
description: "Add your own domain to MachineMail for custom addresses and catch-all: DNS records, verification states, DNS host detection, Domain Connect and the zone file."
canonical: "https://machinemail.dev/docs/concepts/domains"
---

# Domains

> Add your own domain to MachineMail for custom addresses and catch-all: DNS records, verification states, DNS host detection, Domain Connect and the zone file.

Source: https://machinemail.dev/docs/concepts/domains

Inboxes on `machinemail.dev` work out of the box. Add a custom domain when you want addresses like `support@agents.acme.dev`, catch-all addresses, or sending that is authenticated as your own domain. Subdomains work the same as apex domains, and keep your main domain's mail setup untouched.

For a step-by-step walkthrough, see [Use a custom domain](https://machinemail.dev/docs/guides/custom-domain).

## Adding a domain

```bash title="Terminal"
curl https://api.machinemail.dev/v0/domains \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "agents.acme.dev", "catch_all": true, "catch_all_ttl_seconds": 86400}'
```

| Field                   | Notes                                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `domain`                | Required. A hostname. Must not already be registered by any organization.                |
| `inbound_enabled`       | Default `true`. When `false`, the MX record is not required and the domain is send-only. |
| `catch_all`             | Default `false`. Accept mail to any address. See [Catch-all](#catch-all).                |
| `catch_all_ttl_seconds` | Make catch-all inboxes ephemeral with this TTL.                                          |
| `feedback_enabled`      | Default `false`. Stored for AgentMail compatibility.                                     |
| `client_id`             | Idempotency key.                                                                         |

The response lists the DNS records to publish:

```json title="Response"
{
  "domain_id": "dom_01m4fy8hmc0000fpjt8gvxtayy",
  "organization_id": "org_01m4fy8h140000efvagszh6rpx",
  "pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",
  "domain": "agents.acme.dev",
  "status": "NOT_STARTED",
  "reason": "Publish the DNS records, then call POST /v0/domains/{domain_id}/verify",
  "feedback_enabled": false,
  "inbound_enabled": true,
  "catch_all": true,
  "catch_all_ttl_seconds": 86400,
  "nameservers": ["ns23.domaincontrol.com", "ns24.domaincontrol.com"],
  "dns_provider": "godaddy",
  "dns_provider_url": "https://dcc.godaddy.com/control/acme.dev/dns",
  "subdomains_enabled": false,
  "tracking_enabled": false,
  "records": [
    { "type": "TXT", "name": "_dmarc.agents.acme.dev", "value": "v=DMARC1; p=none", "status": "MISSING", "purpose": "dmarc", "required": false },
    { "type": "MX", "name": "agents.acme.dev", "value": "inbound-smtp.us-east-1.amazonaws.com", "priority": 10, "status": "MISSING", "purpose": "inbound", "required": true },
    { "type": "CNAME", "name": "4f3k2j..._domainkey.agents.acme.dev", "value": "4f3k2j....dkim.amazonses.com", "status": "MISSING", "purpose": "dkim", "required": true },
    { "type": "MX", "name": "mf.agents.acme.dev", "value": "feedback-smtp.us-east-1.amazonses.com", "priority": 10, "status": "MISSING", "purpose": "provider", "required": true },
    { "type": "TXT", "name": "mf.agents.acme.dev", "value": "v=spf1 include:amazonses.com ~all", "status": "MISSING", "purpose": "spf", "required": true }
  ],
  "created_at": "2026-10-09T09:00:16.207Z",
  "updated_at": "2026-10-09T09:00:16.207Z"
}
```

Always publish the records exactly as the API returns them for your domain. The set can differ between domains, for example when sending records are issued per domain, in which case `purpose` is `provider`.

## DNS records

| `purpose`  | What it does                                                                                  | Required                               |
| ---------- | --------------------------------------------------------------------------------------------- | -------------------------------------- |
| `inbound`  | MX record that routes mail for the domain to MachineMail.                                     | Yes, when `inbound_enabled` is `true`. |
| `spf`      | Authorises Amazon SES to send for the domain, on the sending subdomain `mf.<domain>`.         | Yes                                    |
| `dkim`     | The key receivers use to check the signature on your mail: three CNAMEs to Amazon SES's keys. | Yes                                    |
| `provider` | The MX of the sending subdomain `mf.<domain>`, where bounces and complaints come back.        | Yes                                    |
| `dmarc`    | A starter DMARC policy.                                                                       | No. Skip it if you already have DMARC. |

Each record has a `status`:

| Record `status` | Meaning                                                                  |
| --------------- | ------------------------------------------------------------------------ |
| `MISSING`       | Nothing found at that name.                                              |
| `INVALID`       | Something is there but it does not match. `reason` shows what was found. |
| `VALID`         | Matches.                                                                 |

An existing SPF record is fine as long as it contains the `include:` from the API's record. Any DMARC record counts as valid.

## Verification

```bash title="Terminal"
curl -X POST https://api.machinemail.dev/v0/domains/dom_01m4fy8hmc0000fpjt8gvxtayy/verify \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

`verify` looks up every record now and returns the updated domain.

| Domain `status` | Meaning                                                                                   |
| --------------- | ----------------------------------------------------------------------------------------- |
| `NOT_STARTED`   | Added but never verified.                                                                 |
| `PENDING`       | Some required records are still missing.                                                  |
| `INVALID`       | A required record exists with the wrong value.                                            |
| `VERIFYING`     | Every required record is valid; Amazon SES has not confirmed them yet.                    |
| `FAILED`        | Amazon SES gave up on the records: DKIM was not found within 72 hours.                    |
| `VERIFIED`      | Every required record is valid. `verified_at` is set and a `domain.verified` event fires. |

`reason` explains what is outstanding, for example `"MX agents.acme.dev is missing"`. It is absent once the domain is verified.

After the first `verify`, MachineMail keeps re-checking `PENDING`, `INVALID` and `VERIFYING` domains in the background about every 5 minutes, for up to 7 days after the domain was added. A domain in `NOT_STARTED` is not checked automatically, so call `verify` once after publishing the records. `last_checked_at` shows when the last check ran.

If a domain is still `NOT_STARTED` 48 hours after it was added, the owners and admins of the organization get an email with its DNS records.

A domain that is never verified is removed 14 days after it was added, together with its inboxes and the mail in them, so that nobody can hold on to a domain they do not own. The owners and admins of the organization get an email at least 3 days before, with the date. A domain that was verified once is never removed this way.

> **Note:** You can create inboxes on a domain right after adding it. Mail only arrives once the MX record resolves, and catch-all only works once the domain is `VERIFIED`. Wait for verification before sending
> from the domain; until SPF and DKIM are in place, receivers are likely to reject or junk your mail.

## DNS host detection

When you add or verify a domain, MachineMail looks up the zone's nameservers and recognises common DNS hosts. The domain then carries:

- `nameservers`: the authoritative nameservers it found.
- `dns_provider`: `cloudflare`, `route53`, `google`, `vercel`, `namecheap`, `godaddy`, `digitalocean`, `hetzner`, `porkbun` or `other`.
- `dns_provider_url`: a link straight to the zone's DNS settings at that host, when known.
- `dns_conflicts`: records the domain already has that conflict with the ones to publish, such as MX records of another mail provider or an SPF record at the same name. Each entry says what was found, why it conflicts and how to resolve it. Conflicts never stop a domain from being added. See [Check for conflicting records](https://machinemail.dev/docs/guides/custom-domain#check-for-conflicting-records).

The dashboard uses these to show an &#x2A;*Open in \[provider]** button on [the domain page](https://machinemail.dev/app/domains).

## Domain Connect

[Domain Connect](https://www.domainconnect.org/) lets a DNS host add records for you after you approve them in its own UI. Cloudflare, GoDaddy, IONOS and others support it. When it works for your zone, the dashboard shows &#x2A;*Add records at \[provider]**, which opens the provider with MachineMail's records prefilled.

```bash title="Terminal"
curl https://api.machinemail.dev/v0/domains/dom_01m4fy8hmc0000fpjt8gvxtayy/domain-connect \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response"
{
  "supported": false,
  "provider_id": "cloudflare.com",
  "provider_name": "Cloudflare",
  "url_sync_ux": "https://dash.cloudflare.com/domainconnect",
  "reason": "template_not_supported",
  "checked_at": "2026-10-09T09:05:12.000Z"
}
```

When `supported` is `true`, `apply_url` is a signed link that adds only the records still outstanding. Open it in a browser (a popup of `width` by `height`, or a new tab). After you approve, the provider sends you back to the domain page in the dashboard, which runs a verify. The domain object also carries a short `domain_connect` summary (`supported`, `provider_name`, `apply_url`) from the last check.

`reason` explains why there is no apply URL:

| `reason`                  | Meaning                                                                         |
| ------------------------- | ------------------------------------------------------------------------------- |
| `no_domain_connect`       | The zone's DNS host does not support Domain Connect.                            |
| `no_sync_flow`            | The host supports Domain Connect but not the synchronous flow MachineMail uses. |
| `template_not_supported`  | The host has not onboarded MachineMail's template yet.                          |
| `records_not_templatable` | The domain's records don't fit the template.                                    |
| `already_configured`      | Every record is already in place.                                               |
| `provider_error`          | The host's Domain Connect API failed. Retried after about 10 minutes.           |
| `not_configured`          | Domain Connect is not available yet.                                            |

> **Warning:** Domain Connect onboarding with DNS providers is still pending. Until MachineMail's template is published and accepted by each provider, discovery returns `template_not_supported` (or
> `records_not_templatable` when the records don't fit the template), and the dashboard falls back to the &#x2A;*Open in \[provider]** console link and the zone file. Add the records by hand in the
> meantime.

## Zone file

`GET /v0/domains/{domain_id}/zone-file` returns the records as a BIND zone file (`text/plain`, downloaded as `agents.acme.dev.zone`). Many DNS hosts, including Cloudflare, can import it.

```bash title="Terminal"
curl https://api.machinemail.dev/v0/domains/dom_01m4fy8hmc0000fpjt8gvxtayy/zone-file \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" -o agents.acme.dev.zone
```

## Catch-all

With `catch_all: true` on a verified domain with inbound enabled, mail to any address at the domain is accepted. The first message to a new address creates the inbox with `created_by: "catch_all"` in the domain's pod. With `catch_all_ttl_seconds` set, those inboxes are ephemeral and are deleted after that long without new mail.

The local part must be a valid username (`a-z`, `0-9` and `. _ + -`). Mail to invalid local parts is not delivered.

Catch-all is off by default and never applies to the shared `machinemail.dev` domain.

## Updating and deleting

`PATCH /v0/domains/{domain_id}` accepts `catch_all`, `catch_all_ttl_seconds` (`null` to clear), `inbound_enabled` and `feedback_enabled`. Turning `inbound_enabled` off makes the MX record optional and recomputes the status.

`DELETE /v0/domains/{domain_id}` removes the domain from MachineMail. Inboxes already created on it are not deleted; delete them separately. Inbox-scoped keys cannot add domains; pod keys see and add domains in their pod only.

## Next

- [Use a custom domain](https://machinemail.dev/docs/guides/custom-domain)
- [Inboxes](https://machinemail.dev/docs/concepts/inboxes)
