Cloudflare Email Routing via API: A Working hello@ Address in Minutes
Set up a forwarding hello@ address with the Cloudflare Email Routing API: destinations, enabling the zone, a literal rule, DNS checks, and error 2007.
- A site’s contact address can be a pure forward: Cloudflare Email Routing receives mail for
[email protected]and passes it to an inbox you already have. No mail server, and nothing extra to pay. - Destination addresses are verified per account, not per domain. If you’ve verified an inbox for another zone before, a new zone can use it straight away.
- Enabling routing on the zone makes Cloudflare add and lock its own MX records and an SPF TXT record. After that you add one rule with a
literalmatcher onto. - Our call to the DNS endpoint with the apex name in the body failed with error 2007, “must be a subdomain”. The older
/enableendpoint, which Cloudflare now marks as deprecated, worked. - Check with real DNS lookups, then send a test message from an outside account. We did the lookups. The test send is still on our list.
Why a working contact address matters
When we rebuilt ailog.page, the contact page needed a real address, not a form that goes nowhere. Reviewers, readers sending corrections, and ad networks checking that a site has a reachable owner all expect one. We picked [email protected]. The domain’s DNS was already on Cloudflare, and we didn’t want to run a mail server or pay for a mailbox just to receive the odd message. Cloudflare Email Routing covers exactly that case: it accepts mail for addresses on your domain and forwards it to an existing inbox.
The whole setup is four API calls and two DNS lookups. Below, the domain is example.com and the identifiers are placeholders. The steps are the ones we ran for ailog.page.
What you need before you start
| Item | Where it comes from |
|---|---|
<zone-id> |
GET /zones?name=example.com, or the zone’s Overview page in the dashboard |
<account-id> |
The account that owns the zone (dashboard URL or GET /accounts) |
<token> |
An API token that can edit Email Routing and DNS on the zone and read (or create) Email Routing destination addresses on the account |
| A destination inbox | Any mailbox you already read, on another domain (shown below as <your-inbox>), verified once |
Scope the token as tightly as you can. It only needs Email Routing and DNS on this one zone, plus the account-level addresses permission.
TOKEN='<token>'; ZONE='<zone-id>'; ACCT='<account-id>'
API=https://api.cloudflare.com/client/v4
H=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json")
Step 1: check destination addresses (they’re account-level)
Destination addresses belong to the account (/accounts/<account-id>/email/routing/addresses), not to a zone. So start by listing what’s already verified:
curl -s "${H[@]}" "$API/accounts/$ACCT/email/routing/addresses" \
| jq -c '.result[] | {email, verified}'
# {"email":"<your-inbox>","verified":"2023-05-27T15:25:29Z"}
In our case the inbox we wanted had been verified back in 2023 on the same account, long before this domain existed. It showed a timestamp in verified, and we could use it straight away. If yours is new, create it with a POST to the same path with {"email":"<your-inbox>"}. Cloudflare sends a confirmation link to that inbox, and rules pointing at it won’t deliver until someone clicks it. That click is the only step that can’t be done through the API.
Check the zone’s current state while you’re at it:
curl -s "${H[@]}" "$API/zones/$ZONE/email/routing" | jq -c '.result | {enabled, status}'
# {"enabled":false,"status":"unconfigured"}
Step 2: enable routing on the zone, and the 2007 error
Enabling Email Routing does the DNS work for you. Cloudflare’s docs describe the current endpoint, POST /zones/<zone-id>/email/routing/dns, as “Enable your Email Routing zone. Add and lock the necessary MX and SPF records.” Its body has an optional name field.
We sent the zone apex as name, and it was refused:
curl -s "${H[@]}" -X POST "$API/zones/$ZONE/email/routing/dns" -d '{"name":"example.com"}' \
| jq -c '{success, errors}'
# {"success":false,"errors":[{"code":2007,"message":"Invalid Input: must be a subdomains of example.com"}]}
For ailog.page the message read “must be a subdomains of ailog.page”. So name is for enabling routing on a subdomain (for example mail.example.com), not for the apex. We then called the older endpoint, which worked and moved the zone to ready:
curl -s "${H[@]}" -X POST "$API/zones/$ZONE/email/routing/enable" \
| jq -c '{success, errors, status: .result.status}'
# {"success":true,"errors":[],"status":"ready"}
Cloudflare’s API reference now marks /email/routing/enable as deprecated in favour of the DNS endpoint. It still worked when we used it, but don’t rely on it in a script you plan to keep. Since name is optional, the documented way to enable the apex is probably POST .../email/routing/dns with no name at all. That fits the docs, but we haven’t tested it, because our zone was already enabled by then.
After enabling, our lookups showed only Cloudflare’s MX records. If yours already receives mail through another provider, decide which one should own the domain’s MX before enabling. Forwarding through Cloudflare and a separate mailbox host can’t both receive mail for the same name.
Step 3: create a literal-match forward rule
A rule is a list of matchers and a list of actions. For a single contact address, use one literal matcher on the to field and one forward action:
curl -s "${H[@]}" -X POST "$API/zones/$ZONE/email/routing/rules" -d '{
"name": "hello -> owner",
"enabled": true,
"matchers": [{ "type": "literal", "field": "to", "value": "[email protected]" }],
"actions": [{ "type": "forward", "value": ["<your-inbox>"] }]
}' | jq -c '{success, errors}'
# {"success":true,"errors":[]}
A few notes:
- Literal means exact. Mail to
Hello@orinfo@won’t match this rule. If you want more addresses, add more rules instead of turning on a catch-all. A catch-all on a public domain forwards every spam guess at random usernames straight to your inbox. - Forward targets must be verified destination addresses from step 1. Until the confirmation link is clicked, nothing gets delivered to them.
- Rules are per zone. Destinations are shared across the account, but each domain needs its own rules.
Read the zone status once more to confirm:
curl -s "${H[@]}" "$API/zones/$ZONE/email/routing" | jq -c '.result | {enabled, status}'
# {"enabled":true,"status":"ready"}
Step 4: verify with DNS lookups (and then a real message)
The API saying ready only reflects Cloudflare’s view. What other mail servers act on is DNS, so query it from a public resolver. We used doggo, but dig works the same way:
doggo example.com MX @1.1.1.1 --short
# route1.mx.cloudflare.net.
# route2.mx.cloudflare.net.
# route3.mx.cloudflare.net.
doggo example.com TXT @1.1.1.1 --short
# "v=spf1 include:_spf.mx.cloudflare.net ~all"
About 20 seconds after enabling, we saw three routeN.mx.cloudflare.net MX records and an SPF record including _spf.mx.cloudflare.net, all added by Cloudflare and none created by hand. The MX priorities Cloudflare assigns aren’t sequential numbers. That’s normal, and there’s nothing to change.
DNS records prove the routing is in place, not that mail arrives. The last check is to send a message to [email protected] from an account outside the domain (a free webmail account is fine) and confirm it lands, including in the spam folder. When this was written, we’d done the API and DNS checks for ailog.page but not the end-to-end test message, so that’s the next item on our list. It’s also worth repeating after any change to the zone’s DNS.
Checklist and things to watch
- Destination inbox is listed under the account with a
verifiedtimestamp. - Zone status is
enabled: true, status: ready. - Public DNS shows Cloudflare’s MX records and the SPF include, with no leftover MX from an old provider.
- One
literalrule per published address. No catch-all unless you really want it. - A test message from an outside account arrived.
- The address on the site’s contact page matches the rule exactly.
Two things to keep in mind afterwards. Email Routing only receives. If you reply from your normal inbox, the reply comes from that inbox’s address, not hello@. Sending as the domain needs a separate mail service with its own SPF and DKIM entries, merged into the same SPF record. And the records are locked while routing is on. If someone later tries to “clean up” DNS, they’ll find they can’t edit those MX records without disabling routing first. That’s by design, and worth noting in whatever runbook covers the domain.