Cloudflare email decoder
Paste the hex of a data-cfemail attribute, a /cdn-cgi/l/email-protection#… link or a whole block of HTML, and this Cloudflare email decoder shows the address behind every [email protected] placeholder. The first byte is the key; every byte after it, XOR the key, is one character. Everything runs in your browser.
Paste anything: every data-cfemail attribute and every email-protection link in the text is decoded. A bare hex string works too, one per line. Real examples from Cloudflare-protected pages, 7 October 2026:
| # | Found in | Key | Address | Notes |
|---|
Show the bytes of the first payload
Hundreds of pages to read? Our web scraping API decodes Cloudflare-protected addresses on every page it fetches, with no browser and no JavaScript. See the request and the response.
email-protection# in a link.How Cloudflare email protection works
Email Address Obfuscation is a Cloudflare setting (part of Scrape Shield). When it is on, Cloudflare's edge rewrites every email address in the HTML before the page leaves the server. An address written as text becomes <span class="__cf_email__" data-cfemail="…">[email protected]</span>; a mailto: link becomes a link to /cdn-cgi/l/email-protection#…. Cloudflare also adds a small script, email-decode.min.js, that puts the real addresses back in the visitor's browser.
A browser with JavaScript never notices. Anything that reads the raw HTML does: curl, Python requests, most crawlers and AI agents get the literal [email protected], and the link points to a /cdn-cgi/ path with no page behind it. On 7 October 2026 Google's own result snippets for Italian dental practices showed [email protected] in place of the address, which is what a non-JavaScript reader sees.
The algorithm in three steps
- Split the hex into bytes. Read the string two digits at a time. The first byte is the key, a number from 0 to 255 that Cloudflare picks for each occurrence.
- XOR every following byte with the key. Each result is one byte of the address.
- Read the bytes as UTF-8, then undo the site's own layers. Some sites mask the address before Cloudflare encodes it: HTML entities (
in…), percent-encoding (%40,%20), amailto:prefix or a?subject=tail. Strip those and the address is left.
The worked example below is the payload drawn in the figure, 5a33343c351a3f223b372a363f74393537, which splits into the bytes 5a 33 34 3c 35 1a 3f 22 3b 37 2a 36 3f 74 39 35 37:
| Byte | XOR key 0x5a | Character |
|---|---|---|
5a | the key (90) | none |
33 | 0x69 | i |
34 | 0x6e | n |
3c | 0x66 | f |
35 | 0x6f | o |
1a | 0x40 | @ |
3f 22 3b … | 0x65 0x78 0x61 … | e x a … |
Sixteen bytes after the key give info@example.com. Because the key changes from one occurrence to the next, the same address can appear on a page under several different hex strings: decode first, then remove duplicates.
Decode it in Python or JavaScript
The tool above is the readable version. In code it is a few lines. Python, standard library only:
import html, re
from urllib.parse import unquote
def decode_cfemail(hex_str: str) -> str | None:
data = bytes.fromhex(hex_str)
key = data[0]
text = bytes(b ^ key for b in data[1:]).decode("utf-8", "replace")
text = unquote(html.unescape(text)) # layers added by the site
m = re.search(r"[\w.+-]+@[\w-]+(\.[\w-]+)+", text)
return m.group(0) if m else None
print(decode_cfemail("5a33343c351a3f223b372a363f74393537")) # info@example.com
JavaScript, in a browser or in Node:
function decodeCfemail(hex) {
const bytes = hex.match(/../g).map((h) => parseInt(h, 16));
const key = bytes[0];
const raw = Uint8Array.from(bytes.slice(1), (b) => b ^ key);
return new TextDecoder().decode(raw);
}
decodeCfemail("5a33343c351a3f223b372a363f74393537"); // "info@example.com"
To find the payloads in a page, read the data-cfemail attribute of every element that has one (it is usually a span, sometimes the a itself) and the part after # in every href that contains /cdn-cgi/l/email-protection.
What we found on 54 real pages
On 7 October 2026 we ran four Google searches for pages whose snippet showed [email protected]: Italian dental practices, law firms, hotels and US dentists. Of the 68 result pages, 54 returned HTML to a plain HTTP request. We decoded every payload in that HTML with the rule on this page.
| Measure | Result |
|---|---|
| Pages still serving Cloudflare email protection | 49 of 54 |
| Encoded payloads in those pages | 508 |
In a data-cfemail attribute / in a link with #hex | 269 / 239 |
| Payloads that decoded to an address | 504 of 508 |
| Share links with no recipient (only a subject or body) | 4 |
| Addresses masked by the site first (percent-encoding) | 3 |
| Pages with the same address under two or more keys | 40 of 49 |
| Distinct addresses recovered, summed per page | 159 |
Two things follow for anyone parsing these pages. The protection is the same everywhere, so one decoder covers every site that uses it. And deduplicating on the hex string does not work, because 40 of the 49 pages repeated an address under different keys.
Why your scraper sees [email protected]
The decoding happens in JavaScript after the page loads. An HTTP client that does not run the page's scripts keeps the placeholder, and a parser that only looks for mailto: links finds none, because they all point to /cdn-cgi/. A headless browser does get the addresses, but it costs a full render for something the HTML already contains. Decoding in the parsing step is cheaper and gives the same result.
If it is your own site
Site owners usually meet this feature from the other side: SEO crawlers report hundreds of links to /cdn-cgi/l/email-protection as broken, because those URLs only work with JavaScript. You can switch Email Address Obfuscation off for the whole zone in the Cloudflare dashboard, or keep it and exclude single blocks by wrapping them in <!--email_off--> and <!--/email_off-->. Either way, the obfuscation is a speed bump: as this page shows, it is reversible by design.
Decode at scale: the scraping API does it on every page
Our web scraping API decodes Cloudflare email protection natively, on the plain HTTP tier, with no browser and no JavaScript. It handles data-cfemail on a span, an a or any other tag; /cdn-cgi/l/email-protection#… links, relative or absolute; protection links without the #hex (the decoded label is used); and the protection URL written as plain text. It also removes the layers sites add before Cloudflare: HTML entities, %40-style encoding, the mailto: prefix and the ?subject= tail.
- Markdown and text: the address in clear, with a
mailto:link. On zkdental.it the link went from[info@zkdental.it](https://www.zkdental.it/cdn-cgi/l/email-protection#7355…)to[info@zkdental.it](mailto:info@zkdental.it). - HTML: the page byte for byte as served, except the placeholders, which become the address and
href="mailto:…". - metadata.contactEmails: every address with its source, in this order:
json-ld,cloudflare,mailto, without duplicates. - metadata.contactPhones: the numbers from JSON-LD and
tel:links, as{ phone, source }.
One request, on 7 October 2026, to the contact page of an Italian lighting manufacturer that protects every address with Cloudflare:
curl https://api.quanticdata.io/v1/scrape \
-H "Authorization: Bearer $QD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.reggiani.net/it/contatti/", "format": "markdown"}'
{
"type": "response",
"message": "Extraction successful",
"payload": {
"url": "https://www.reggiani.net/it/contatti/",
"status": 200,
"engine": "tls",
"metadata": {
"contactEmails": [
{ "email": "contact@reggiani.net", "source": "cloudflare" },
…
],
"contactPhones": [
{ "phone": "00390297070340", "source": "tel" }
]
},
"content": "…[contact@reggiani.net](mailto:contact@reggiani.net)…"
}
}
That single response carried 59 addresses in metadata.contactEmails, every one with "source": "cloudflare", on the plain HTTP tier in 7.3 seconds.
| Setting | Value |
|---|---|
| Endpoint | POST /v1/scrape (alias /v1/scraper/extract) |
| Engine | auto: plain HTTP first, a browser only when the page needs it |
| Price | $0.0002 per page, $0.001 when a browser render is needed; failed pages are not billed |
| Many pages | POST /v1/batch: a list of URLs in one async job, at $0.0002 per URL, same output per URL |
| Free tier | $2 of free API usage per month, no card |
Collect business contact data only where you have a lawful basis for it: in the EU that means GDPR applies to a named person's work address too. Public company addresses used for B2B outreach, with an opt-out, are the common case; consumer data and bulk mailing without a basis are not.
Stop decoding by hand
The same request decodes every protected address on every page you send it, at $0.0002 per page. Your key comes with $2 of free API usage per month.
Get my free API key Read the scrape docs$2 of free API usage per month, no credit card.
Sources and standards
What this tool implements, so you can check it yourself: