APP_EXPERIMENTAL=mail (see Experimental features). While it is off the routes answer 404.
The zone must live at a connected DNS provider (see DNS zones and records) and its name must equal the domain.
Plan, apply, check
plan reads the live zone and prints what would be added, left unchanged, replaced or refused. It writes nothing. apply needs the hash from the plan and --confirm; it plans again and refuses with 409 if the zone or your flags changed since, so a stale plan never applies. Use the same flags on both. Applying writes an audit entry with record names and types only. Records are written one set at a time, not as one atomic batch, but every record is validated before the first write.
API: POST /api/v1/mail/dns/plan, POST /api/v1/mail/dns/apply (body needs confirm: true and plan_hash), GET /api/v1/mail/dns/check?domain=.
Detect the DNS host, then apply in one step
levelrail-cli mail dns provider example.com (API GET /api/v1/mail/dns/provider?domain=, and GET /api/v1/domains/dns-provider?domain= outside the mail flag) looks up the public NS records, walking up from a subdomain to the first name that has them, and names the host: Cloudflare, Route 53, Google Cloud DNS, DigitalOcean, Azure, Hetzner, Namecheap, GoDaddy, or manual with the raw nameservers. It also says whether a connected credential can see that zone (connected) and whether that credential could later issue certificates over DNS-01 (can_issue_certificates). Results are cached for APP_MAIL_DNS_PROVIDER_CACHE_SECONDS (default 300). A team member sees connected, zone_id and dashboard_url only for zones holding a domain of their own team.
It also reads _domainconnect.<apex> (then the zone found above) and reports domain_connect with the published host. This is discovery only: nothing calls that host yet. For a connected Cloudflare zone whose account id is known, dashboard_url links straight to its DNS records page.
mail dns plan and apply without --provider use the connected provider the nameservers point at. mail dns domains lists candidate domains (install, claimed, app domains, managed zones) and mail dns applied example.com lists every record mail DNS added, with the provider record ids, read from the audit log. On Cloudflare each record carries the comment managed by levelrail mail.
Least privilege Cloudflare token: Zone, DNS, Edit on the single zone plus Zone, Zone, Read. Nothing more is needed.
Mail records are never proxied. The Cloudflare proxy carries only HTTP, so a proxied mail host, MX target, autoconfig or mta-sts name silently breaks SMTP and IMAP. Every record mail DNS creates is DNS only, and the plan is refused with 409 and a plain reason when an existing proxied record would shadow one of those names. check also returns ptr_hint: reverse DNS is set at the hosting provider that owns the server IP, named when its PTR reveals it.
For app domains the one-step flow already exists: POST /api/v1/apps/{name}/domains/go-live/plan and POST /api/v1/apps/{name}/domains/{domain}/go-live (CLI domains go-live).
Add-only by default
Changing mail records can break a live domain. By default nothing existing is overwritten:- If the domain already has MX records that point elsewhere, the plan is refused with a clear message.
- An existing SPF, DKIM, DMARC or other record with different content is refused, not merged.
- Other TXT values at the same name (such as site verification tokens) are never touched. Replacing SPF edits only the
v=spf1value.
--replace-existing MX,SPF. Valid kinds are MX, HOST, SPF, DKIM, DMARC, MTASTS, TLSRPT and AUTOCONFIG. The plan shows each value it would remove.
What each record is
Record status
mail dns status <domain> (GET /api/v1/mail/dns/status), domains dns-status <domain> [--app NAME] (GET /api/v1/domains/dns-status), the plan, the check and a go-live plan sent with "status": true report each record with what already exists (found). When a connected provider can see the zone and you may see it, the zone listing is used (provider_source: provider); otherwise public DNS (public_dns).
Results are reused for
APP_DNS_STATUS_CACHE_SECONDS (default 3, 0 disables) and identical concurrent checks share one lookup. APP_DNS_STATUS_LOOKUP_TIMEOUT, APP_DNS_STATUS_PROVIDER_TIMEOUT and APP_DNS_STATUS_CONCURRENCY bound the lookups.
Provider presets
--preset fills the MX, SPF include and fixed DKIM records of a hosted provider. Included: google, fastmail, proton, zoho (US data center), and the sending-only ses, mailgun and postmark. Each preset notes where its values come from and which records still come from the provider’s dashboard (per-account DKIM keys, verification tokens). Check them against the provider’s current instructions before applying.
Deliverability reality
DNS is the easy part. For a server you run:- Many cloud hosts block outbound port 25 by default. Ask the host to open it.
checkreports this as not tested. - The sending IP needs reverse DNS (PTR) that resolves back to the mail host name. You set PTR at the IP owner, not in your zone.
checkverifies it. - A new IP has no reputation. Warm it up slowly, keep DMARC reports flowing, and expect early mail to land in spam.
- Residential IP ranges are usually rejected outright.