How to find a Turnstile sitekey on a page
The Turnstile sitekey is the data-sitekey attribute on the element with class cf-turnstile — open the page source and search for 0x4, and the 24-character string you land on is the key. If the widget renders explicitly the value is a JavaScript argument instead, and you read it from the turnstile.render call or from the widget iframe's URL.
What the sitekey is, and why it is public
Every Turnstile widget has two halves. The sitekey identifies the widget to Cloudflare's client-side script and ships in the page to every visitor. The secret key validates tokens against the siteverify endpoint and belongs only on your server. Finding a sitekey is not a leak — it is how the widget works at all.
<!-- implicit rendering: the sitekey is right there in the markup --> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <form action="/login" method="POST"> <div class="cf-turnstile" data-sitekey="0x4AAAAAAAAA_target" data-theme="auto"></div> <button type="submit">Sign in</button> </form>
| Sitekey | Secret key | |
|---|---|---|
| Where it lives | Client HTML and JS, visible to everyone | Server environment only |
| Shape | Starts 0x4, usually 24 characters | Starts 0x4, roughly 35 characters |
| Used for | Rendering the widget and requesting a token | POST /turnstile/v0/siteverify to validate a token |
| If it becomes public | Nothing to do — it already is | Rotate it in the Cloudflare dashboard immediately |
| Send it to a solver | Yes, that is the input | Never |
A sitekey is bound to the hostnames configured on the widget. The same key will not produce a valid token from an unrelated domain, which is why any solve has to be tied to the real page URL, not just the key.
Page source first, DevTools second
Most sites render the widget server-side, so the key is in the HTML before a single line of JavaScript runs. Fetch the page and grep it — faster than a browser and scriptable in CI.
curl -s https://staging.example.com/login \
| grep -oE '(data-sitekey="[^"]+"|0x4[A-Za-z0-9_-]{10,})' \
| sort -uIf that returns nothing, the widget is injected at runtime. Open DevTools, press Ctrl+Shift+F (Cmd+Opt+F on macOS) to search across every loaded resource, and search for 0x4. That one string finds the key in markup, in bundled JavaScript, in JSON config blobs and in network URLs at the same time. Four places worth checking, in order:
- The
cf-turnstileelement'sdata-sitekeyattribute — the normal case. - A
sitekey:property inside an application bundle, for explicit rendering. - The widget iframe's
src, which carries the key as a path segment underchallenges.cloudflare.com/cdn-cgi/challenge-platform/. - A framework-specific hiding place — a meta tag or an inline JSON config the front end reads on boot.
The same job in Node with no dependencies, when you want it in a script rather than a shell:
// find-sitekey.mjs — Node 18+, no dependencies // usage: node find-sitekey.mjs https://staging.example.com/login const url = process.argv[2]; if (!url) { console.error("usage: node find-sitekey.mjs <url>"); process.exit(1); } const res = await fetch(url, { headers: { "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", }, }); const html = await res.text(); const patterns = [ /data-sitekey=["']([^"']+)["']/gi, // implicit rendering /["']?sitekey["']?\s*:\s*["']([^"']+)["']/gi, // explicit rendering config /(0x4[A-Za-z0-9_-]{10,})/g, // anything key-shaped ]; const keys = new Set(); for (const re of patterns) { for (const m of html.matchAll(re)) keys.add(m[1]); } console.log( keys.size ? [...keys].join("\n") : `no sitekey in the HTML of ${res.url} (status ${res.status}) — render the page instead`, );
Explicit rendering: the sitekey is an argument
When the script is loaded with ?render=explicit, there is no cf-turnstile div to read. The site calls turnstile.render() itself and passes the key in the options object, usually from an onload callback.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback&render=explicit" defer ></script> <div id="widget"></div> <script> // the sitekey never appears as an HTML attribute here window.onloadTurnstileCallback = function () { turnstile.render("#widget", { sitekey: "0x4AAAAAAAAA_target", callback: function (token) { console.log("token", token); }, }); }; </script>
In a minified bundle the property name survives minification because it is a string key in an object literal, so searching the Sources panel for sitekey finds the call site. The reliable fallback needs no source reading at all: let the widget render, then read the key out of the iframe URL. Cloudflare puts it in the path, and it is there for implicit, explicit, managed, non-interactive and invisible widgets alike.
Extract it programmatically with Playwright
For a page that only assembles the widget client-side, drive a real browser. This script watches every request for a key-shaped string, then also reads any data-sitekey attributes and every frame URL. It covers all the rendering modes in one pass.
# pip install playwright && playwright install chromium import re import sys from playwright.sync_api import sync_playwright KEY = re.compile(r"0x4[A-Za-z0-9_-]{10,}") def find_sitekeys(url: str, timeout_ms: int = 15000) -> list[str]: found: list[str] = [] with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() # The widget iframe URL carries the sitekey as a path segment, so watch # the network instead of guessing when the DOM has settled. page.on("request", lambda r: found.extend(KEY.findall(r.url))) page.goto(url, wait_until="domcontentloaded") try: page.wait_for_selector( "[data-sitekey], iframe[src*='challenges.cloudflare.com']", timeout=timeout_ms, ) except Exception: pass # no widget on this page, or it never rendered # Attributes cover test keys, which do not start with 0x4. found += page.eval_on_selector_all( "[data-sitekey]", "els => els.map(e => e.getAttribute('data-sitekey'))", ) for frame in page.frames: found.extend(KEY.findall(frame.url)) browser.close() return list(dict.fromkeys(k for k in found if k)) # de-duped, order kept if __name__ == "__main__": keys = find_sitekeys(sys.argv[1]) print("\n".join(keys) if keys else "no turnstile sitekey found")
Run it headed with p.chromium.launch(headless=False) if nothing comes back — some widgets hold off rendering until the form scrolls into view or a field gets focus.
Keys you will find that are not real, and pages with no key at all
If the key you extracted does not start with 0x4, check it against Cloudflare's published dummy keys before you build anything on it. Staging environments run on these constantly, and they never talk to a real challenge.
| Dummy sitekey | Behaviour |
|---|---|
1x00000000000000000000AA | Always passes, visible widget |
2x00000000000000000000AB | Always fails, visible widget |
1x00000000000000000000BB | Always passes, invisible widget |
2x00000000000000000000BB | Always fails, invisible widget |
3x00000000000000000000FF | Forces an interactive challenge |
The other case is a page with no sitekey to find. A full-page Cloudflare interstitial — the "Checking your browser" hold served by the WAF in front of the origin — is not embedded by the site author, so there is no cf-turnstile element and no key in anyone's markup. That is a different gate. SolveGate handles it as gate="waf" against the URL rather than as a widget solve.
Passing the sitekey to a solve
Once you have the key, a solve needs two inputs: the sitekey and the URL of the page the widget renders on. SolveGate returns the token the widget would have produced, typically in under 1.5 seconds.
# pip install solvegate (Python 3.9+) import os from solvegate import SolveGate sg = SolveGate(os.environ["SOLVEGATE_KEY"]) # sk_live_… or a free sk_test_… key res = sg.solve( gate="turnstile", sitekey="0x4AAAAAAAAA_target", # the value you just extracted url="https://staging.example.com/login", # the page the widget is on ) print(res.status, res.solve_ms, "ms") print(res.token)
Feed res.token into the hidden input the widget would have filled — cf-turnstile-response, unless the page renamed it with data-response-field-name — then submit the form as usual. For an explicit-render integration, hand the token to the callback function instead. Tokens are single-use and short-lived, so request one at the moment you submit rather than caching it. Failed solves are never billed, the first 1,000 solves are free, and credits run from $0.40 per 1,000 down to $0.075 at volume.
Do this against properties you own or have written authorisation to test — your own staging logins, CI end-to-end suites, uptime checks and anti-bot configuration testing. Automating someone else's Turnstile without permission is out of scope here and prohibited by SolveGate's acceptable use policy.
Never send the secret key anywhere. It has no role in obtaining a token, only in verifying one on your own server, and any service that asks for it is asking for the wrong thing.
Common questions
In the data-sitekey attribute of the element with class cf-turnstile. If the site renders explicitly, it is the sitekey property in the turnstile.render() options object instead, and it also appears as a path segment in the widget iframe's src under challenges.cloudflare.com/cdn-cgi/challenge-platform/.
Production sitekeys start with 0x4 and are usually 24 characters, for example 0x4AAAAAAABUYP0XeMJF0xoy. Cloudflare's dummy testing keys are the exception — they start with 1x, 2x or 3x and never clear a real challenge.
Yes. The sitekey is served to every visitor by design, because the browser needs it to render the widget. The secret key is the sensitive half: it validates tokens through siteverify and must stay on your server. If a secret key has been exposed, rotate it in the Cloudflare dashboard.
The widget is being injected at runtime. Load the page in a real browser and read the key from the rendered DOM or the widget iframe URL — the Playwright script above does both. If there is still no key and the page is a full-screen "Checking your browser" interstitial, there is no sitekey to find: that is a WAF challenge page, not an embedded widget.
For widgets you own, yes — Turnstile in the Cloudflare dashboard lists every widget with its sitekey and secret key. Reading it from the page is what you do when the widget belongs to an environment you are testing but did not configure yourself.
No. Each widget is configured with a hostname list, and a token minted for the wrong hostname fails verification. Always pair the sitekey with the real URL the widget renders on when you request a solve.
Related
More in Guides
Automating a gate you own or are authorised to test?
// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free