guides · turnstile · sitekey

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.

html
<!-- 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>
SitekeySecret key
Where it livesClient HTML and JS, visible to everyoneServer environment only
ShapeStarts 0x4, usually 24 charactersStarts 0x4, roughly 35 characters
Used forRendering the widget and requesting a tokenPOST /turnstile/v0/siteverify to validate a token
If it becomes publicNothing to do — it already isRotate it in the Cloudflare dashboard immediately
Send it to a solverYes, that is the inputNever

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.

bash
curl -s https://staging.example.com/login \
  | grep -oE '(data-sitekey="[^"]+"|0x4[A-Za-z0-9_-]{10,})' \
  | sort -u

If you would rather not do this by hand every time, our Turnstile checker runs exactly this fetch-and-parse pass on a URL and reports the sitekey, the rendering mode and the widget's data-* configuration. It reads the served HTML, so it finds what curl finds and stops where curl stops.

If 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-turnstile element's data-sitekey attribute — 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 under challenges.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:

js
// 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.

html
<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.

python
# 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 sitekeyBehaviour
1x00000000000000000000AAAlways passes, visible widget
2x00000000000000000000ABAlways fails, visible widget
1x00000000000000000000BBAlways passes, invisible widget
2x00000000000000000000BBAlways fails, invisible widget
3x00000000000000000000FFForces an interactive challenge

The other case is a page with no sitekey to find. A Cloudflare interstitial is served in front of the origin and may have its own challenge parameters rather than the site owner's embedded widget. Check the cf-mitigated: challenge response header. For SolveGate, use gate="waf", the target URL and the required non-empty sitekey="waf" placeholder; that provider uses the URL and proxy rather than a widget key.

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. A successful SolveGate request returns a token for that widget.

python
# 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 the response has cf-mitigated: challenge, it is a Cloudflare challenge page. Use SolveGate's WAF contract rather than assuming it is an embedded widget; Supported gates explains its required placeholder and clearance response.

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

Sources

More in Guides

Playwright and Cloudflare TurnstileStop Cloudflare Turnstile breaking Playwright tests: use Cloudflare's official test sitekeys on staging, or solve a real widget and inject the token.Python Cloudflare Turnstile SolverSolve Cloudflare Turnstile from Python with plain requests or the solvegate SDK. Complete runnable code, async usage, retries, and error handling.Node.js Turnstile solverSolve Cloudflare Turnstile from Node.js: a zero-dependency global fetch version, the solvegate npm SDK, async/await error handling and TypeScript types.How to get the cf-turnstile-response tokenWhat the cf-turnstile-response token is, the hidden input it lives in, how siteverify validates it, and how to get one in your own automated tests.Puppeteer Turnstile bypassHow to handle Cloudflare Turnstile in Puppeteer: Cloudflare's official test sitekeys for pages you own, plus a runnable token-injection script.Selenium and Cloudflare Turnstile in PythonHandle Cloudflare Turnstile in Selenium 4 and Python: test sitekeys, explicit waits, execute_script token injection, and what changes in headless.Turnstile fails on a datacenter IPThe same code passes on your laptop and loops forever on a server. What Cloudflare documents about IP reputation, what it does not, and what actually changes.curl_cffi and Turnstilecurl_cffi impersonates TLS and HTTP/2 fingerprints and has no JavaScript runtime. Its own FAQ says so. What that solves, what it cannot, and where the line is.Scrapy and TurnstileScrapy has no JavaScript engine, so a Turnstile page returns markup and no data. The three documented routes, and which one Scrapy's own docs recommend first.undetected-chromedriver and TurnstileIt binary-patches one string out of chromedriver. Its own README says it does not hide your IP and that headless is unfinished. What that means for Turnstile.SeleniumBase and TurnstileThe uc_gui captcha methods use PyAutoGUI and raise in headless mode — the check is in the source. solve_captcha uses CDP and does not. Which to use where.Camoufox and TurnstileCamoufox is a Firefox fork that patches fingerprints at the C++ level. It ships no Turnstile solver, and the issue asking for one was closed as not planned.nodriver and Turnstilenodriver's README documents a tab.cf_verify() that clicks the Cloudflare checkbox. It is not in the shipped code. What the library does do instead.Patchright and TurnstilePatchright is a drop-in Playwright that closes the Runtime.enable leak and reaches into closed shadow roots — which is exactly where a Turnstile checkbox lives.DrissionPage and TurnstileDrissionPage reaches into non-open shadow roots and switches between browser and HTTP mode. It also forbids commercial use, and its docs are Chinese-only.Go, Colly and TurnstileColly has no JavaScript engine — its dependency graph proves it. What that means for Turnstile in Go, and how chromedp and rod compare when you need a browser.

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