Why can my domain receive email but not send any?
Cloudflare Email Routing forwards inbound mail only. It has no sending path, so anything transactional needs a separate provider called over an HTTP API. Add a sending domain, create the DNS records the provider returns, verify it, and keep the routing MX on the apex so receiving continues to work.
Inbound mail worked, replies never arrived
We enabled Cloudflare Email Routing on the zone, pointed one address at a destination inbox we already owned, and inbound mail arrived on the first test. The MX records had been created for us. Nothing suggested anything was missing, so the line item was ticked and we moved on.
The gap surfaced later, when the first account on the site needed a password reset. Nothing left the site. There was no bounce, no queue and no mail log, because there was no sending path to log anything. We had built a receiving system and assumed it ran in both directions. Outbound mail was unavailable for , which is the expensive part of this failure rather than the repair.
Two smaller things in the same session read as faults and were not. A forwarding rule created before the destination inbox was confirmed returned error 2054, and the contact address we published in plain HTML was absent from the page source, replaced by an encoded /cdn-cgi/l/email-protection link.
Routing forwards mail; it does not send it
Cloudflare Email Routing is a forwarder and only a forwarder. It receives mail addressed to your domain and hands it on to a destination inbox you already have. Enabling it auto-creates the MX records, you get one rule per address and one destination per rule, and the feature is free. There is no outbound endpoint, no SMTP credential and no sending quota, because sending is not what the product does.
What catches people is structural. Every visible step of setting up email is present: DNS records appear, an address exists, a test message arrives. The missing half produces no error, so the only signal is the first outbound message that never sends.
Anything transactional needs a separate sending provider: account created, password reset, trial ending, receipts, login links. In this network that provider is Resend, called over its HTTP API rather than over SMTP, which keeps sending inside the same automated provisioning path as everything else.
The two systems coexist cleanly. The sending provider's MX sits on a send. subdomain, so it does not conflict with the Email Routing MX on the apex. Both can be present at once, which is the arrangement you want: receive through routing, send through the API.
The destination confirmation is the one step that cannot be automated. A new destination inbox must be added and then confirmed by a human clicking a verification link in it. Creating a forwarding rule before that click returns error 2054, which is expected rather than a fault.
Check the records, then send a real message
Start with DNS, because the record shapes tell you which halves exist. Query each over DNS-over-HTTPS and compare to the value the provider gave you.
curl -s -H "accept: application/dns-json" \
"https://cloudflare-dns.com/dns-query?name=<record>&type=TXT"
An apex MX with no send. MX alongside it is the signature of this failure: receiving is configured and sending was never set up. Present send. records with a domain still reporting pending is a different situation, and the answer is to wait.
Check which address your application sends from. A provider's default sandbox sender only delivers to the account owner's own address. It is adequate for proving a pipeline runs and useless for real customers, and it produces the worst test result available: a message that arrives for you and nobody else.
Then send a real end-to-end test and pick the destination deliberately. Do not use Gmail or Outlook as the target. Gmail silently dedupes self-sent test mail, and a provider's own send test email button has the same flaw. Test to an address on a provider you are not otherwise involved with, so that a missing message means a missing message.
Finally, read the raw HTML of your contact page. A mailto: rewritten to /cdn-cgi/l/email-protection is Cloudflare's Email Address Obfuscation hiding the address from crawlers and from anyone reading the source.
Add a sending service and its DNS records
- Keep Email Routing for inbound mail. Add the destination inbox, open it, and click the verification link. Only then create the forwarding rules, one rule per address and one destination per rule.
- Add the sending domain to the transactional provider by POSTing the domain to its API. The response carries the exact DNS records the domain needs, so nothing has to be read off a dashboard or typed by hand.
- Create each returned record through the Cloudflare DNS API. The MX record needs a priority value from the provider response, the one field that is easy to drop when looping over records.
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"MX","name":"send","content":"<provider mx host>","priority":<priority>,"ttl":1}' - Add the optional DMARC record as a TXT record at
_dmarcwith the valuev=DMARC1; p=none;. - POST to the provider's verify endpoint, then poll the domain until it reports verified. Expect
pendingfor a while. Records can resolve publicly within seconds while the provider still reports pending, because the check runs through an upstream mail service. - If it stays pending, confirm the records resolve and match the expected values with the query above, then wait. Do not re-add records and do not assume a mistake. Re-adding during verification is how a wait becomes an outage.
- Move the application off the sandbox sender onto an address on the verified domain before anyone outside the account can trigger an email.
- Turn off Email Address Obfuscation for the zone so your published contact address stays readable.
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/<zone_id>/settings/email_obfuscation" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value":"off"}' - Send one live message end to end from the application, using the real sender address, to a mailbox on a third provider.
Test both directions on launch day
Treat inbound and outbound as two systems on the launch checklist, with two separate verification lines. A single item reading "email working" is how this failure survives to production.
Put the destination inbox confirmation on the checklist as its own step and flag it as manual. Everything else here can be automated, so the one click that cannot be is the step that gets forgotten.
Automate the whole sending domain setup as one function: POST the domain, create every returned record, add the _dmarc TXT record, POST to verify, poll until verified. Run it during provisioning so a new site has a verified sending domain before it has a signup form.
Write the expected verification wait into the runbook. We budget , so that pending is read as normal progress rather than as a broken setup.
Assert at deploy time that the sender address sits on a verified domain rather than a sandbox one, add the obfuscation setting to the per-zone bootstrap so it is off on every site from the start, and keep a live send test in the deploy checks aimed at a mailbox nobody on the team uses day to day.
Deliverability once sending works
A verified sending domain is permission to send, not a guarantee of inbox placement. Nothing here addresses reputation, content filtering or the history of a shared sending IP, and a p=none DMARC record enforces nothing on its own.
None of it makes Email Routing send mail. Remove the transactional provider and you are back to a receive-only domain.
It does not solve replies typed by a human. Forwarding delivers inbound mail into an inbox you already own, and a reply written there goes out under that inbox's own domain unless you configure a send-as identity.
Turning off obfuscation is a trade rather than a win: the address becomes readable to anything reading your source. And this is a delivery mechanism only. It has no bearing on consent for anything resembling marketing mail, or on the unsubscribe handling such mail requires.
Questions about sending from your own domain
Can Cloudflare Email Routing send email?
No. It receives mail addressed to your domain and forwards it to a destination inbox you already have, and that is the whole feature. Sending requires a separate transactional provider called over its own API.
What does Cloudflare error 2054 mean when I add a forwarding rule?
It means the destination inbox has not been confirmed yet. A human has to open that inbox and click the verification link, and until that happens the rule cannot be created. The error is expected rather than a fault.
Why does my sending domain still say pending when the DNS records resolve?
The provider's check runs through an upstream mail service, not through a public resolver, so records can resolve within seconds while the domain still reports pending. Confirm the records match the expected values, then wait without re-adding anything.
Will the sending provider's MX record conflict with Email Routing?
No. The sending provider's MX sits on a send. subdomain while Email Routing's MX sits on the apex, so both can exist at once. That coexistence is the arrangement you want.
Why did my test email arrive but a customer's did not?
You were most likely still sending from the provider's default sandbox address, which only delivers to the account owner's own address. It is fine for proving the pipeline runs and useless for real recipients.
Why is my mailto link showing as /cdn-cgi/l/email-protection?
Cloudflare's Email Address Obfuscation rewrites every mailto: in your HTML into an encoded link. Turn it off per zone with a PATCH to /zones/<zone_id>/settings/email_obfuscation setting the value to off.