<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harry Stone</title>
    <description>The latest articles on DEV Community by Harry Stone (@harrysstone).</description>
    <link>https://dev.to/harrysstone</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185084%2F2bbf0763-dfaf-430f-a1cf-5deb080904e2.png</url>
      <title>DEV Community: Harry Stone</title>
      <link>https://dev.to/harrysstone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harrysstone"/>
    <language>en</language>
    <item>
      <title>Delegation vs emulation is exactly the split that made the difference in our production agent setup.</title>
      <dc:creator>Harry Stone</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:20:02 +0000</pubDate>
      <link>https://dev.to/harrysstone/delegation-vs-emulation-is-exactly-the-split-that-made-the-difference-in-our-production-agent-setup-22g6</link>
      <guid>https://dev.to/harrysstone/delegation-vs-emulation-is-exactly-the-split-that-made-the-difference-in-our-production-agent-setup-22g6</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/hellowow/captcha-handling-for-ai-agents-delegation-vs-emulation-4odi" class="crayons-story__hidden-navigation-link"&gt;CAPTCHA Handling for AI Agents: Delegation vs Emulation&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/hellowow" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3737133%2F78475a39-1073-47f8-a715-9d5953ad6bb8.png" alt="hellowow profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/hellowow" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Christina • AltWeb Team
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Christina • AltWeb Team
                
                
              
              &lt;div id="story-author-preview-content-3205669" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/hellowow" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3737133%2F78475a39-1073-47f8-a715-9d5953ad6bb8.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Christina • AltWeb Team&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/hellowow/captcha-handling-for-ai-agents-delegation-vs-emulation-4odi" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 28&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/hellowow/captcha-handling-for-ai-agents-delegation-vs-emulation-4odi" id="article-link-3205669"&gt;
          CAPTCHA Handling for AI Agents: Delegation vs Emulation
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/hellowow/captcha-handling-for-ai-agents-delegation-vs-emulation-4odi" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/hellowow/captcha-handling-for-ai-agents-delegation-vs-emulation-4odi#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Getting a Cloudflare Turnstile token in Puppeteer without breaking your flow</title>
      <dc:creator>Harry Stone</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:42:23 +0000</pubDate>
      <link>https://dev.to/harrysstone/getting-a-cloudflare-turnstile-token-in-puppeteer-without-breaking-your-flow-66m</link>
      <guid>https://dev.to/harrysstone/getting-a-cloudflare-turnstile-token-in-puppeteer-without-breaking-your-flow-66m</guid>
      <description>&lt;p&gt;Cloudflare Turnstile has quietly become the default "are you a robot?" gate on new sites — and it is where most Puppeteer flows die. Unlike reCAPTCHA, it does not expose a global configuration object you can poke; the token it produces is single-use and short-lived; and the widget fingerprints the browser hard enough that a plain headless launch often fails before a human would ever see a challenge.&lt;/p&gt;

&lt;p&gt;This post walks the whole pipeline: where to get the sitekey, how to capture the page's callback, how to fetch a valid token from a solving API, and how to hand it back without breaking your flow. Code is Node.js + Puppeteer. &lt;strong&gt;Heads-up: the snippets were checked against a local mock of the solving API and a stub Turnstile widget, not against a live Cloudflare-protected site&lt;/strong&gt; — treat them as a well-commented starting point and test against a test sitekey first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the reCAPTCHA trick does not work for Turnstile
&lt;/h2&gt;

&lt;p&gt;If you automated reCAPTCHA v2 before, you probably injected the token through the global config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;___grecaptcha_cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works because reCAPTCHA keeps its per-page configuration in a global object (&lt;code&gt;___grecaptcha_cfg&lt;/code&gt;). Turnstile has no such global you can rely on: the widget is rendered per container, and the callback only exists as an option passed to &lt;code&gt;turnstile.render()&lt;/code&gt; — after that it lives in a closure you cannot reach.&lt;/p&gt;

&lt;p&gt;That is exactly the dead end in &lt;a href="https://stackoverflow.com/questions/79027476" rel="noopener noreferrer"&gt;this Stack Overflow thread&lt;/a&gt;: the author got a token from a solving service, set it on &lt;code&gt;cf-turnstile-response&lt;/code&gt;, and then had no idea how to trigger the site's validation, because the page has no visible form — it just waits. The top answers there (set the hidden input's &lt;code&gt;.value&lt;/code&gt;; fire a synthetic &lt;code&gt;submit&lt;/code&gt;) work in some setups and miss the main path in others.&lt;/p&gt;

&lt;p&gt;The reliable move: &lt;strong&gt;intercept &lt;code&gt;turnstile.render&lt;/code&gt; before the page's own scripts run&lt;/strong&gt;, capture the parameters (sitekey, action, cData) and the callback, and later call that callback with a token you got from elsewhere. That is precisely what the site's code would have called had the widget succeeded on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Turnstile token gets into the page
&lt;/h2&gt;

&lt;p&gt;In a normal human flow, three things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The page calls &lt;code&gt;turnstile.render(container, { sitekey, callback, 'error-callback', action, cData })&lt;/code&gt; — or, with implicit rendering, puts the same options on a &lt;code&gt;&amp;lt;div class="cf-turnstile" data-sitekey="…" data-callback="…"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The widget creates a hidden &lt;code&gt;&amp;lt;input name="cf-turnstile-response"&amp;gt;&lt;/code&gt; and, on success, fills it and calls &lt;code&gt;callback(token)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The token is sent to the server (via the callback's logic or a form submit) and verified once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two consequences for automation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are single-use and expire quickly.&lt;/strong&gt; The server verifies them; a token you solved ten minutes ago — or already submitted — will not pass a second time. Solve fresh, inject immediately, never cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser and the connection are fingerprinted.&lt;/strong&gt; The widget inspects browser signals such as WebGL output, and Cloudflare scores the connection itself (TLS fingerprints such as JA3); there is a long &lt;a href="https://news.ycombinator.com/item?id=48345840" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt; about headless and privacy browsers getting blocked. In practice: a flagged headless Chromium may never produce a token on its own, and even after you inject a good token, a later re-challenge may fail. Decide early whether you need headful or stealth mode — that is a separate problem from token injection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 0 — when you do not need a solving API at all
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The site is yours (QA / test automation).&lt;/strong&gt; Cloudflare publishes &lt;a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" rel="noopener noreferrer"&gt;test sitekeys&lt;/a&gt; — &lt;code&gt;1x00000000000000000000AA&lt;/code&gt; always passes. Point your staging environment at it and your flow tests what you actually care about. No API, no cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real headful browser just passes.&lt;/strong&gt; Managed challenges often solve themselves in a normal headful session without a click. If your flow tolerates that, it beats any API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You only test your own backend.&lt;/strong&gt; If you verify tokens server-side, issue them with the test key and hit your endpoint directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The site's rules allow it.&lt;/strong&gt; A token API solves the captcha, not the terms of service — check the site's ToS/robots expectations before automating a third-party property at scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A token API earns its keep when the site belongs to someone else, you run headless at scale, and you cannot wait for a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — capture sitekey, action, cData and the callback
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;page.evaluateOnNewDocument&lt;/code&gt; runs before the page's own JavaScript — the right moment to wrap &lt;code&gt;turnstile.render&lt;/code&gt;. A property trap catches the moment Cloudflare's script assigns &lt;code&gt;window.turnstile&lt;/code&gt;, so even a page that renders immediately on load goes through the wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateOnNewDocument&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__wrapped&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__tsParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__tsCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="c1"&gt;// Let the real widget render so the hidden input exists&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;original&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turnstile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;configurable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The trap is needed because &lt;code&gt;window.turnstile&lt;/code&gt; only appears after Cloudflare's script loads, and some pages call &lt;code&gt;render&lt;/code&gt; in the same tick.&lt;/li&gt;
&lt;li&gt;With implicit rendering there is no &lt;code&gt;render&lt;/code&gt; call from the page: read &lt;code&gt;data-sitekey&lt;/code&gt;, &lt;code&gt;data-action&lt;/code&gt;, &lt;code&gt;data-cdata&lt;/code&gt; and the &lt;code&gt;data-callback&lt;/code&gt; function name from the &lt;code&gt;.cf-turnstile&lt;/code&gt; element instead.&lt;/li&gt;
&lt;li&gt;Letting the original &lt;code&gt;render&lt;/code&gt; run means the hidden &lt;code&gt;cf-turnstile-response&lt;/code&gt; input exists in the DOM even if the widget itself fails — you will need it as a fallback.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;window.__tsParams.sitekey&lt;/code&gt; answers "where do I get the sitekey": it is the exact value the page itself uses. Some sites use per-page keys, so reading &lt;code&gt;data-sitekey&lt;/code&gt; from the raw HTML is a second-best check, not the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2 — get a token from a solving service
&lt;/h2&gt;

&lt;p&gt;For sites you do not own, a token-solving API does the work of a human in a real browser: you send &lt;code&gt;pageurl&lt;/code&gt; + &lt;code&gt;sitekey&lt;/code&gt; (plus &lt;code&gt;action&lt;/code&gt;, and the page's &lt;code&gt;cData&lt;/code&gt; as &lt;code&gt;data&lt;/code&gt;, if the page sets them) and receive a token valid for that page.&lt;/p&gt;

&lt;p&gt;SolveCaptcha is one such service — hybrid: AI where it is reliable, human solvers where it is not — at &lt;strong&gt;$0.80 per 1,000 Turnstile tokens&lt;/strong&gt; (rate checked 2026-10-19, &lt;a href="https://solvecaptcha.com/" rel="noopener noreferrer"&gt;solvecaptcha.com&lt;/a&gt;). Its API is two HTTP calls: submit to &lt;code&gt;in.php&lt;/code&gt;, then poll &lt;code&gt;res.php&lt;/code&gt; until the token is ready; there is also a &lt;code&gt;solvecaptcha-javascript&lt;/code&gt; npm module that wraps the same flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.solvecaptcha.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;solveTurnstileToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pageurl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cData&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turnstile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageurl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// With json=1 both endpoints answer {"status": 0|1, "request": "..."}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/in.php?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`in.php: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// documented poll interval&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/res.php?`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAPCHA_NOT_READY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the token&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`res.php: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer not to hand-roll the polling, the &lt;code&gt;solvecaptcha-javascript&lt;/code&gt; module exposes the same call as &lt;code&gt;solver.cloudflareTurnstile({ pageurl, sitekey, action, data })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two honesty notes. First, no 100%-success promises: expect the occasional bad solve, so treat a page-level "verification failed" as a retryable event, not a crash. Second, full-screen Turnstile &lt;strong&gt;challenge pages&lt;/strong&gt; (the interstitial variant, not an embedded widget) are a separate, harder case — check the service's documentation for that variant before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — inject the token without breaking the flow
&lt;/h2&gt;

&lt;p&gt;Replay what the widget would have done. Prefer the callback; fall back to the hidden input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__tsCallback&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;__tsCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// what the site expects on success&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="cf-turnstile-response"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If the flow continues via a real form/button:&lt;/span&gt;
&lt;span class="c1"&gt;// await page.click('#your-submit-button');&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Callback first.&lt;/strong&gt; If the site wired a callback (typical for SPAs and "just wait" pages with no visible form), the callback is the only thing that moves the flow — this is the exact dead end from the Stack Overflow thread above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input + events&lt;/strong&gt; covers sites that read the field on submit. Dispatching &lt;code&gt;input&lt;/code&gt;/&lt;code&gt;change&lt;/code&gt; matters because some frontends validate on change, not on submit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fire a synthetic &lt;code&gt;submit&lt;/code&gt; blindly&lt;/strong&gt; (some SO answers do): on SPAs it can run handlers twice or too early. Click the real button or call the real handler.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What breaks in practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token accepted once, then rejected&lt;/strong&gt; → reused or expired token. Solve fresh per attempt; never reuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callback called, nothing happens&lt;/strong&gt; → the page re-rendered the widget and now listens to a different callback. Reload and re-capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sitekey captured&lt;/strong&gt; → the page uses implicit rendering (no &lt;code&gt;render&lt;/code&gt; call). Read &lt;code&gt;data-sitekey&lt;/code&gt; and &lt;code&gt;data-callback&lt;/code&gt; from the &lt;code&gt;.cf-turnstile&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works headful, fails headless&lt;/strong&gt; → fingerprinting, not the token. Go headful or stealth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CAPCHA_NOT_READY&lt;/code&gt; for a long time&lt;/strong&gt; → service load or a wrong &lt;code&gt;pageurl&lt;/code&gt;/&lt;code&gt;sitekey&lt;/code&gt;. Back off and verify the parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two habits: send the exact &lt;code&gt;pageurl&lt;/code&gt; your page is on (the token is bound to it), and do not poll faster than the documented interval — you only burn money.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stop and rethink
&lt;/h2&gt;

&lt;p&gt;A token API is a bridge, not a moat. If you are re-challenged on every navigation, the problem is your fingerprint, your datacenter proxy, or the site's risk scoring — not the token. And for "login → Turnstile → dashboard" flows, a headful session with a warm cookie jar will often outlast a headless one.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turnstile has no &lt;code&gt;___grecaptcha_cfg&lt;/code&gt;. Wrap &lt;code&gt;turnstile.render&lt;/code&gt; via &lt;code&gt;evaluateOnNewDocument&lt;/code&gt;; capture &lt;code&gt;sitekey&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;cData&lt;/code&gt;, and the callback.&lt;/li&gt;
&lt;li&gt;Fetch a fresh token from a solving API with the exact &lt;code&gt;pageurl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inject via the captured callback; fall back to &lt;code&gt;cf-turnstile-response&lt;/code&gt; + input/change events.&lt;/li&gt;
&lt;li&gt;Tokens are single-use and short-lived: solve fresh, inject fast, retry politely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If this saved you an afternoon, follow for more scraping-and-automation notes — and tell me in the comments how Turnstile is breaking your flow.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/79027476" rel="noopener noreferrer"&gt;How to inject a Cloudflare Turnstile token into Puppeteer — Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://solvecaptcha.com/captcha-solver-api" rel="noopener noreferrer"&gt;SolveCaptcha API documentation (in.php / res.php, Turnstile section)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://solvecaptcha.com/pricing" rel="noopener noreferrer"&gt;SolveCaptcha pricing&lt;/a&gt; — Cloudflare Turnstile $0.8 per 1000, checked 2026-10-19&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/solvercaptcha/solvecaptcha-javascript" rel="noopener noreferrer"&gt;solvecaptcha-javascript&lt;/a&gt; — Turnstile parameters and example&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=48345840" rel="noopener noreferrer"&gt;Cloudflare Turnstile requiring fingerprintable WebGL — Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/" rel="noopener noreferrer"&gt;Cloudflare Turnstile widget configurations&lt;/a&gt; — &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;cData&lt;/code&gt;, callbacks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" rel="noopener noreferrer"&gt;Cloudflare Turnstile testing&lt;/a&gt; — test sitekeys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: this article was drafted with AI assistance and reviewed by a human, in line with Dev.to's guidelines on disclosing AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>puppeteer</category>
      <category>javascript</category>
      <category>webscraping</category>
      <category>captcha</category>
    </item>
  </channel>
</rss>
