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

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