Skip to content
machinemail

Use a custom domain

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

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.

  1. Add the domain

    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}'
    FieldDefaultMeaning
    domainrequiredThe hostname. A subdomain such as agents.acme.dev keeps agent mail apart from your people’s mail.
    inbound_enabledtrueReceive mail at this domain. When false, the MX record is not required and the domain is send-only.
    catch_allfalseAccept mail for any address at the domain, creating the inbox on first delivery.
    catch_all_ttl_secondsnoneMake catch-all inboxes ephemeral with this TTL.
    feedback_enabledfalseAgentMail field, stored and returned.
    client_idnoneIdempotency key. A repeat with the same value returns the existing domain.

    You can also add a domain in the dashboard at /app/domains. The key needs domain_create. Each domain can belong to one organization only; adding one that is already registered returns 409 conflict.

  2. Read the DNS records

    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.

    Response (201, trimmed)
    {
      "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": "MX", "name": "agents.acme.dev", "value": "mx.machinemail.dev", "priority": 10, "status": "MISSING", "purpose": "inbound", "required": true },
        { "type": "TXT", "name": "agents.acme.dev", "value": "v=spf1 include:spf.machinemail.dev ~all", "status": "MISSING", "purpose": "spf", "required": true },
        { "type": "TXT", "name": "machinemail._domainkey.agents.acme.dev", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqh...", "status": "MISSING", "purpose": "dkim", "required": true },
        { "type": "TXT", "name": "_dmarc.agents.acme.dev", "value": "v=DMARC1; p=none", "status": "MISSING", "purpose": "dmarc", "required": false }
      ]
    }
    PurposeRecordWhy
    inboundMX to mx.machinemail.dev, priority 10Routes mail for the domain to MachineMail. Required only when inbound_enabled is true.
    spfTXT v=spf1 include:spf.machinemail.dev ~allAuthorizes MachineMail to send for the domain. If you already have an SPF record, add the include: to it instead of publishing a second one.
    dkimTXT at machinemail._domainkey.<domain>Public key for the signature on your outgoing mail.
    dmarcTXT at _dmarc.<domain>A starting DMARC policy. Optional: any existing DMARC record counts.
    providervariesExtra records some domains need for sending. Publish them when present.

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

  3. Publish the records

    MachineMail looks up the domain’s nameservers when you add it and reports the DNS host in dns_provider (cloudflare, route53, google, vercel, namecheap, godaddy, digitalocean, hetzner, porkbun or other), with a deep link to that host’s DNS console in dns_provider_url when one is known.

    You have three ways to get the records into your zone.

    By hand. Open dns_provider_url (the dashboard’s Open in Provider button) 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):

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

    The dashboard has the same download as the Zone file button on /app/domains/<domain_id>.

    Domain Connect (one click). Some DNS hosts, Cloudflare included, support Domain Connect. When yours does, the dashboard shows Add records at Provider. It opens your DNS host’s own approval screen with the MachineMail records prefilled. You approve, the host adds them, and you land back on the domain page, which runs a verification.

    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 expect supported: false with reason: "template_not_supported", and use the console link or the zone file.

    To check Domain Connect from the API:

    Terminal
    curl https://api.machinemail.dev/v0/domains/dom_01m4fy8hmc0000fpjt8gvxtayy/domain-connect \
      -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
    Response (today, for a Cloudflare zone)
    {
      "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:

    reasonMeaning
    no_domain_connectYour DNS host does not support Domain Connect.
    template_not_supportedYour DNS host supports it but has not accepted the MachineMail template yet.
    no_sync_flowYour DNS host only offers a flow MachineMail does not use.
    records_not_templatableThis domain’s records do not fit the template, for example because it needs provider records.
    already_configuredNothing is left to add (supported is true, there is no apply_url).
    provider_errorThe DNS host did not answer. Retried after 10 minutes.
    not_configuredDomain Connect is turned off.

    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.

  4. Verify

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

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

    The response is the domain with fresh per-record status (MISSING, INVALID or VALID) and an overall status:

    Domain statusMeaning
    NOT_STARTEDNever verified.
    PENDINGSome required records are still missing.
    INVALIDA required record exists with the wrong value.
    VERIFIEDEvery required record is valid. verified_at is set.

    Call 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.

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

  5. Create inboxes on the domain

    Pass domain when you create an inbox:

    Terminal
    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.

    Note

    The API lets you create inboxes on a domain before it verifies, but mail will not arrive until the MX record is live, and receivers are likely to reject or junk what you send until SPF and DKIM pass. Verify first.

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:

  • the domain is VERIFIED,
  • inbound_enabled is true,
  • the local part is a valid username: lowercase letters, digits and . _ + -, 1 to 64 characters, starting and ending with a letter or digit.

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.

Turn catch-all on or off later with PATCH /v0/domains/{domain_id}, which also accepts catch_all_ttl_seconds (null to clear it), inbound_enabled and feedback_enabled.

Warning

A catch-all domain accepts mail for every address, including guesses and spam traps. Every message still gets a trust report, but use a dedicated subdomain for catch-all rather than your main domain.

Troubleshooting

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

Record (example)
{
  "type": "TXT",
  "name": "agents.acme.dev",
  "value": "v=spf1 include:spf.machinemail.dev ~all",
  "status": "INVALID",
  "purpose": "spf",
  "required": true,
  "reason": "Found: v=spf1 include:_spf.google.com ~all"
}
SymptomFix
SPF INVALIDYou have an SPF record without the MachineMail include. Add include:spf.machinemail.dev to it. Do not publish two SPF records.
MX INVALIDAnother MX record answers for the name. For a subdomain like agents.acme.dev, put the MX on the subdomain, not on acme.dev.
DKIM MISSINGSome DNS consoles append the zone to the name. Enter machinemail._domainkey.agents in a zone for acme.dev, not the full name.
DKIM INVALIDThe key was cut or wrapped. Long TXT values are split into quoted chunks in the zone file; paste the value from the API as one string.
Everything MISSINGDNS has not propagated yet, or the records are in a different zone than the nameservers in nameservers.

DELETE /v0/domains/{domain_id} removes the domain. Remove the DNS records afterwards.