custom domain.md


title: "Use a custom domain" description: "Add your own domain to MachineMail: publish MX, SPF, DKIM and DMARC records by hand, from a zone file or with Domain Connect, verify it and turn on catch-all." canonical: "https://machinemail.dev/docs/guides/custom-domain"

Use a custom domain

Add your own domain to MachineMail: publish MX, SPF, DKIM and DMARC records by hand, from a zone file or with Domain Connect, verify it and turn on catch-all.

Inboxes on the shared machinemail.dev domain work with no setup. Add your own domain when you want agents to send from your brand, want unlimited addresses, or want catch-all addresses that exist without an API call.

The flow is: add the domain, publish its DNS records, verify, then create inboxes on it. Each step shows the dashboard first and the API second. Pick a tab once and the page remembers it.

Add the domain

Dashboard

  1. Open machinemail.dev/app/domains?create=1, or go to Domains and choose Add domain.
  2. Enter the Domain. A subdomain such as agents.acme.dev keeps your main domain's mail untouched.
  3. Leave Inbound mail on to receive mail at the domain. Turn it off for a send-only domain.
  4. Turn on Catch-all to accept mail for any address at the domain. *Ephemeral TTL (hours)* then sets how long those inboxes live after their last message. 0 keeps them forever.
  5. Choose Add domain. The domain's page opens with its DNS records.

The Add domain button is turned off when your plan's custom-domain limit is reached. Domains you already have keep working.

API

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 Default Meaning
domain required The hostname. A subdomain such as agents.acme.dev keeps agent mail apart from your people's mail.
inbound_enabled true Receive mail at this domain. When false, the MX record is not required and the domain is send-only.
catch_all false Accept mail for any address at the domain, creating the inbox on first delivery.
catch_all_ttl_seconds none Make catch-all inboxes ephemeral with this TTL.
feedback_enabled false AgentMail field, stored and returned.
client_id none Idempotency key. A repeat with the same value returns the existing domain.

The key needs domain_create.

Each domain can belong to one organization only. Adding one that is already registered fails with a conflict error (409 from the API).

Read the DNS records

Dashboard

The domain's page lists the records under DNS records. Each row shows the Type (with the record's purpose under it), the Name, the Value, the Prio for MX records and the Status. The copy buttons next to each name and value copy the full text.

API

The response lists the records to publish. The values below are typical; always publish exactly what the API returns for your domain, because the DKIM key is unique per domain and the set can differ.

{
  "domain_id": "dom_01m4fy8hmc0000fpjt8gvxtayy",
  "domain": "agents.acme.dev",
  "status": "NOT_STARTED",
  "reason": "Publish the DNS records, then call POST /v0/domains/{domain_id}/verify",
  "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",
  "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 }
  ]
}

MachineMail sends and receives through Amazon SES, and the records come from it, so publish them exactly as listed. There are three DKIM CNAMEs; the example above shows one.

Purpose Record Why
inbound MX on the domain, priority 10 Routes mail for the domain to MachineMail. Required only when inbound_enabled is true.
dkim Three CNAMEs under _domainkey.<domain>, to dkim.amazonses.com The key for the signature on your outgoing mail.
spf TXT v=spf1 include:... ~all on the sending subdomain (mf.<domain>) Authorizes Amazon SES to send for the domain. If you already have an SPF record at that name, add the include: to it instead of publishing a second one.
dmarc TXT at _dmarc.<domain> A starting DMARC policy. Optional: any existing DMARC record counts.
provider MX on the sending subdomain (mf.<domain>) Amazon SES's MAIL FROM domain: bounces and complaints come back through it.

required: true records must all be valid before the domain verifies.

Publish the records

MachineMail looks up the domain's nameservers when you add it and works out your DNS host: Cloudflare, Route 53, Google Cloud DNS, Vercel, Namecheap, GoDaddy, DigitalOcean, Hetzner, Porkbun, or another host.

Dashboard

The DNS host card above the records names your DNS host and its nameservers. You have two ways to get the records in.

By hand. When MachineMail knows your host's DNS console, the card has a button named after it, such as Open in Cloudflare or Open in GoDaddy. It opens the console in a new tab. Create each record there, copying the name and value from the table.

Zone file. Choose Zone file at the top of the page. It downloads a BIND zone file with the records. Import it if your DNS host supports imports (Cloudflare does).

API

The domain reports the DNS host in dns_provider (cloudflare, route53, google, vercel, namecheap, godaddy, digitalocean, hetzner, porkbun or other). When the host is known, dns_provider_url is a deep link to its DNS console.

By hand. Open dns_provider_url and create each record.

Zone file. Download a BIND zone file with the records and import it, if your DNS host supports imports (Cloudflare does):

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

One-click setup (Domain Connect)

Warning: Domain Connect is not available yet. Each DNS host has to accept the MachineMail template before the one-click flow works, and that onboarding, including with Cloudflare, is still in progress. Until then, publish the records by hand or with the zone file.

Some DNS hosts support Domain Connect. Once it is live for yours, the DNS host card also shows Add records at followed by your host's name. It opens your host's approval screen with the MachineMail records filled in. You approve there, and MachineMail checks DNS when you come back.

To check Domain Connect from the API:

curl https://api.machinemail.dev/v0/domains/dom_01m4fy8hmc0000fpjt8gvxtayy/domain-connect \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
{
  "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:10:00.000Z"
}

When supported is true the response also has apply_url, a signed link to your DNS host. Open it in a browser (as a popup of width by height pixels when those are set). It only adds the records that are still missing: the MX if inbound is enabled, SPF and DKIM, and DMARC only when the domain has no DMARC record at all. When supported is false, reason says why:

reason Meaning
no_domain_connect Your DNS host does not support Domain Connect.
template_not_supported Your DNS host supports it but has not accepted the MachineMail template yet.
no_sync_flow Your DNS host only offers a flow MachineMail does not use.
records_not_templatable This domain's records do not fit the template, for example because it needs provider records.
already_configured Nothing is left to add (supported is true, there is no apply_url).
provider_error The DNS host did not answer. Retried after 10 minutes.
not_configured Domain Connect is not available yet.

Every domain object also carries a short domain_connect summary (supported, provider_name, apply_url) from the last check. Discovery results are cached for 6 hours and refreshed on every verify.

Verify

DNS changes can take minutes to propagate. Then run the checks.

Dashboard

Choose Verify now at the top of the domain's page. A message tells you how many records are valid, or that the domain is verified.

API

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

The response is the domain with a fresh status on each record (MISSING, INVALID or VALID) and an overall status. Verify needs the domain_update permission.

The domain's overall status:

Domain status Meaning
NOT_STARTED Never verified.
PENDING Some required records are still missing.
INVALID A required record exists with the wrong value.
VERIFIED Every required record is valid. verified_at is set.

Verify at least once. After that, MachineMail re-checks PENDING and INVALID domains in the background about every 5 minutes for 7 days after the domain was added, so a slow DNS change is picked up without polling. A domain in NOT_STARTED is not re-checked on its own.

Verify within 14 days. A domain that is never verified is removed 14 days after it was added, so a domain added by someone who does not own it cannot keep its owner from adding it. The owners and admins of your organization get an email at least 3 days before, with the date. The inboxes on the domain are removed with it, with the mail in them. A domain that was verified once is never removed this way, even if its records stop checking out later.

When the domain becomes verified, MachineMail emits domain.verified with the domain in data.domain. Subscribe a webhook to it if you automate onboarding.

Create inboxes on the domain

Dashboard

  1. Open machinemail.dev/app/inboxes?create=1, or go to Inboxes and choose Create inbox.
  2. Enter the Address, then pick your domain from the list after the @. The list shows only verified domains.
  3. Optionally, set a Display name. It is shown in the From header of mail the inbox sends.
  4. Choose Create inbox.

API

Pass domain when you create an inbox:

curl https://api.machinemail.dev/v0/inboxes \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username": "billing", "domain": "agents.acme.dev", "display_name": "Acme billing agent"}'

Inboxes on your own domains do not count toward the per-organization cap on the shared domain.

Catch-all addresses

With catch_all: true, mail to any address at the domain is accepted, and the inbox is created on first delivery. That needs all of:

Catch-all inboxes have created_by: "catch_all" and emit inbox.created like any other. If the domain has catch_all_ttl_seconds, they are ephemeral: they expire that long after the last message arrives and are then deleted with their mail. Inbox IDs are lowercased, so Signup+GitHub@agents.acme.dev lands in signup+github@agents.acme.dev.

To change these settings later:

Dashboard

Open the domain from machinemail.dev/app/domains. The Receiving panel has three settings:

API

Use PATCH /v0/domains/{domain_id}. It accepts catch_all, catch_all_ttl_seconds (null to clear it), inbound_enabled and feedback_enabled.

Troubleshooting

When verification fails, the domain's reason lists the problem records, and each INVALID record has its own reason showing what DNS returned:

{
  "type": "TXT",
  "name": "mf.agents.acme.dev",
  "value": "v=spf1 include:amazonses.com ~all",
  "status": "INVALID",
  "purpose": "spf",
  "required": true,
  "reason": "Found: v=spf1 include:_spf.google.com ~all"
}
Symptom Fix
SPF INVALID You have an SPF record at that name without the include from the API (for example include:amazonses.com). Add it; do not publish two SPF records.
MX INVALID Another MX record answers for the name. For a subdomain like agents.acme.dev, put the MX on the subdomain, not on acme.dev.
DKIM MISSING Some DNS consoles append the zone to the name. In a zone for acme.dev, enter <selector>._domainkey.agents, not the full name.
DKIM INVALID The CNAME points somewhere else, or your DNS host proxies it. Point it exactly at the value from the API and turn proxying off for the three CNAMEs.
Everything MISSING DNS has not propagated yet, or the records are in a different zone than the nameservers in nameservers.

To remove a domain, open it in the dashboard. Under Danger zone, choose the delete button, which names the domain (for example Delete agents.acme.dev), and type DELETE to confirm. From the API, call DELETE /v0/domains/{domain_id}. Sending and receiving on the domain stop at once. Inboxes on it stay until you delete them. Remove the DNS records afterwards.