<?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: jujucodings</title>
    <description>The latest articles on DEV Community by jujucodings (@circuitsavage).</description>
    <link>https://dev.to/circuitsavage</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%2F4115690%2F50516f8f-4397-4daa-a8e8-5a69f837f106.jpg</url>
      <title>DEV Community: jujucodings</title>
      <link>https://dev.to/circuitsavage</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/circuitsavage"/>
    <language>en</language>
    <item>
      <title>Cloudscraper Not Working in 2026? Here's Why, and the Fix</title>
      <dc:creator>jujucodings</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:09:22 +0000</pubDate>
      <link>https://dev.to/circuitsavage/cloudscraper-not-working-in-2026-heres-why-and-the-fix-3fhl</link>
      <guid>https://dev.to/circuitsavage/cloudscraper-not-working-in-2026-heres-why-and-the-fix-3fhl</guid>
      <description>&lt;p&gt;You pip-installed cloudscraper, pointed it at a Cloudflare site, and got a &lt;code&gt;403&lt;/code&gt;, a &lt;code&gt;503&lt;/code&gt;, or an endless "Just a moment…" page instead of your data. It's not your code. cloudscraper stopped being able to solve most Cloudflare sites a while ago, and here's the precise reason, plus a fix that keeps your existing &lt;code&gt;requests&lt;/code&gt;-style code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 40-word version
&lt;/h2&gt;

&lt;p&gt;cloudscraper only ever solved Cloudflare's old JavaScript-math challenge. Modern sites use Turnstile or the managed 5-second challenge, which run real browser JavaScript. cloudscraper has no browser and no JS engine, so it can't solve them. It just hands you the challenge page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cloudscraper breaks now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It solved one specific thing: the legacy IUAM challenge.&lt;/strong&gt; cloudscraper reads Cloudflare's old "I'm Under Attack" page, extracts a JavaScript math problem, and evaluates it. That's the whole mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has no browser.&lt;/strong&gt; Turnstile and the managed challenge execute obfuscated JavaScript and collect behavioral and fingerprint signals. There's nothing for a regex-and-eval tool to compute, so the request stays on the challenge page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its "Turnstile support" was always a hand-off.&lt;/strong&gt; cloudscraper never solved Turnstile itself. It added hooks to pass the job to a paid CAPTCHA API and inject the returned token. Without one wired up, that does nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's effectively unmaintained.&lt;/strong&gt; The last real release was in 2023. Cloudflare has shipped a lot of detection changes since.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to confirm that's what's happening
&lt;/h2&gt;

&lt;p&gt;Look at the body you're getting back, not just the status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://protected.example/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# 403 / 503
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-turnstile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Just a moment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True -&amp;gt; it's the challenge
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that's &lt;code&gt;True&lt;/code&gt;, you received the challenge page, not a block. You don't need better headers or a new user-agent. You need something that can actually pass Turnstile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: change one import
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://pypi.org/project/cloudscraper-turnstile/" rel="noopener noreferrer"&gt;cloudscraper-turnstile&lt;/a&gt; for exactly this. It subclasses &lt;code&gt;requests.Session&lt;/code&gt; the same way cloudscraper does and mirrors its API, so migration is one line. When it hits a Turnstile or 5s challenge, it solves it through &lt;a href="https://peak.fo/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=cloudscraper" rel="noopener noreferrer"&gt;Peak&lt;/a&gt; and retries the request transparently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;cloudscraper&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;turnstile&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;PEAK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pk_your_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cloudscraper_turnstile&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cloudscraper&lt;/span&gt;   &lt;span class="c1"&gt;# the only line that changes
&lt;/span&gt;
&lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloudscraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_scraper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://protected.example/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Turnstile solved, real page returned
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your existing &lt;code&gt;create_scraper&lt;/code&gt; keyword arguments (&lt;code&gt;browser&lt;/code&gt;, &lt;code&gt;delay&lt;/code&gt;, &lt;code&gt;sess&lt;/code&gt;, and the rest) are accepted and don't raise, so you don't have to rewrite the rest of your call. Cookies from the solve, including &lt;code&gt;cf_clearance&lt;/code&gt;, persist on the session for every later request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use your own proxy
&lt;/h3&gt;

&lt;p&gt;Cloudflare ties clearance to the requesting IP, so pass a proxy and it's used for the solve too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloudscraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_scraper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk_your_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@ip:port&lt;/span&gt;&lt;span class="sh"&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;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;You pay only for solves that land (failed solves are free), starting at $0.90 per 1,000 and dropping to $0.35 at volume. New accounts get 1,000 free solves with no card, so you can run it against your own target before deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're not on cloudscraper
&lt;/h2&gt;

&lt;p&gt;Same idea, different library: &lt;a href="https://pypi.org/project/scrapy-turnstile/" rel="noopener noreferrer"&gt;scrapy-turnstile&lt;/a&gt;, &lt;a href="https://pypi.org/project/selenium-turnstile/" rel="noopener noreferrer"&gt;selenium-turnstile&lt;/a&gt;, &lt;a href="https://pypi.org/project/playwright-turnstile/" rel="noopener noreferrer"&gt;playwright-turnstile&lt;/a&gt;, and &lt;a href="https://pypi.org/project/turnstile-curl/" rel="noopener noreferrer"&gt;turnstile-curl&lt;/a&gt; for curl_cffi. All of them wrap the same one-call Peak API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://peak.fo/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=cloudscraper" rel="noopener noreferrer"&gt;Get a free key&lt;/a&gt; · &lt;a href="https://peak.fo/docs/turnstile?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=cloudscraper" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/CircuitSavage/cloudscraper-turnstile" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Use this for automation, QA, and scraping public data you're allowed to access. Respect each site's Terms of Service and robots.txt, and rate-limit yourself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Cloudflare Turnstile 403: Why Your Scraper Is Blocked (and How to Fix It)</title>
      <dc:creator>jujucodings</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:25:29 +0000</pubDate>
      <link>https://dev.to/circuitsavage/cloudflare-turnstile-403-why-your-scraper-is-blocked-and-how-to-fix-it-459j</link>
      <guid>https://dev.to/circuitsavage/cloudflare-turnstile-403-why-your-scraper-is-blocked-and-how-to-fix-it-459j</guid>
      <description>&lt;p&gt;You point your scraper at a page and instead of HTML you get a &lt;code&gt;403&lt;/code&gt;. Or worse: the Turnstile widget loads, spins, and never resolves — an infinite loop that eats your timeout budget and returns nothing. You swap proxies. You bolt on stealth plugins. Same wall.&lt;/p&gt;

&lt;p&gt;Here's what's actually happening, and the one thing that reliably gets you past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a real browser still fails
&lt;/h2&gt;

&lt;p&gt;Turnstile isn't a checkbox you click. It's a background challenge that scores the request across two axes: the &lt;strong&gt;browser environment&lt;/strong&gt; (canvas, WebGL, timing, a hundred small signals) and the &lt;strong&gt;IP reputation&lt;/strong&gt;. Get either wrong and the token never issues — the widget just retries, which is the loop you're staring at.&lt;/p&gt;

&lt;p&gt;Automation frameworks fix the first axis reasonably well now. Playwright, Selenium with undetected-chromedriver, camoufox, botasaurus — they drive a real Chromium with plausible fingerprints. But they can't fix the second one. Your traffic is coming from a datacenter IP (AWS, GCP, a cheap VPS, or a datacenter proxy), and Cloudflare has known those ranges for years. A perfect browser on a burned IP still scores as a bot. That's the 403.&lt;/p&gt;

&lt;p&gt;So the two dead ends people burn days on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More stealth.&lt;/strong&gt; Helps the fingerprint, does nothing for the IP. You'll shave the block rate, not remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More proxies.&lt;/strong&gt; Rotating through more datacenter IPs is rotating through more IPs Cloudflare already distrusts. Residential helps, but it's expensive and you're still solving the challenge in-browser, which is slow and brittle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix: get the token out-of-band, then inject it
&lt;/h2&gt;

&lt;p&gt;You don't need your scraper's browser to &lt;em&gt;pass&lt;/em&gt; Turnstile. You need a valid &lt;code&gt;cf-turnstile-response&lt;/code&gt; token for that sitekey, from somewhere that scores clean. Get the token from a solving API, drop it into the response field, and continue.&lt;/p&gt;

&lt;p&gt;The token is what the site actually validates server-side (via &lt;code&gt;siteverify&lt;/code&gt;), and it isn't bound to your scraper's IP on most deployments — which is exactly why this works. One request in, a token out, keep moving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Read the sitekey off the page (the data-sitekey attribute on .cf-turnstile)
&lt;/span&gt;&lt;span class="n"&gt;sitekey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x4AAAAAAA...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://the-site-you-are-scraping.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Solve it via Peak — one call, token back in about a second
&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.peak.fo/solve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk_your_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TurnstileTaskProxyLess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitekey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Submit the token with your request (form field, or inject into the widget)
#    e.g. include cf-turnstile-response=&amp;lt;token&amp;gt; in the POST body / headers the site expects
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole pattern. &lt;code&gt;TurnstileTaskProxyLess&lt;/code&gt; lets the solver supply a clean exit IP so you don't have to bring one. If the target binds the token to the requesting IP (rare, but it happens on hardened setups), switch to &lt;code&gt;task_type: "turnstiletask"&lt;/code&gt; and pass your own &lt;code&gt;proxy&lt;/code&gt; so the solve and your request share an address.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're already in a framework
&lt;/h2&gt;

&lt;p&gt;You usually don't have to hand-roll the request. Drop-in wrappers keep your existing code and just make the 403 go away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitSavage/scrapy-turnstile" rel="noopener noreferrer"&gt;scrapy-turnstile&lt;/a&gt; — a Scrapy middleware&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitSavage/playwright-turnstile" rel="noopener noreferrer"&gt;playwright-turnstile&lt;/a&gt; and &lt;a href="https://github.com/CircuitSavage/selenium-turnstile" rel="noopener noreferrer"&gt;selenium-turnstile&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitSavage/cloudscraper-turnstile" rel="noopener noreferrer"&gt;cloudscraper-turnstile&lt;/a&gt; and &lt;a href="https://github.com/CircuitSavage/turnstile-curl" rel="noopener noreferrer"&gt;turnstile-curl&lt;/a&gt; (curl_cffi, no browser)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitSavage/crawl4ai-turnstile" rel="noopener noreferrer"&gt;crawl4ai-turnstile&lt;/a&gt; for AI-scraping pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does the Turnstile widget loop forever instead of erroring?
&lt;/h3&gt;

&lt;p&gt;Because the challenge is retrying, not failing outright. Cloudflare scored the request too low to issue a token but not low enough to hard-block, so the widget keeps re-attempting. From your side it looks like a hang; underneath, it's a soft fail on IP or fingerprint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the token tied to my IP?
&lt;/h3&gt;

&lt;p&gt;On most Turnstile deployments, no — the site validates the token against the sitekey and hostname, not the solving IP, which is why a token solved elsewhere still passes. A minority of hardened setups do bind it; for those, solve through the same proxy you'll use for the request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I pay for failed solves?
&lt;/h3&gt;

&lt;p&gt;Not with Peak — you're billed only when a valid token comes back. Turnstile is $0.90 per 1,000 (dropping to $0.35 at volume), token in about a second. See the &lt;a href="https://peak.fo/docs/turnstile?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=turnstile-403" rel="noopener noreferrer"&gt;Turnstile docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://peak.fo/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=turnstile-403" rel="noopener noreferrer"&gt;Grab a free key&lt;/a&gt; — 1,000 solves, no card — and run it against the site that's 403-ing you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For legitimate automation, QA, and scraping of data you're authorized to access. Respect each site's Terms of Service and robots.txt.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>webscraping</category>
      <category>python</category>
      <category>scraping</category>
    </item>
    <item>
      <title>The Cloudflare 5-Second Challenge, Explained (and how to pass it)</title>
      <dc:creator>jujucodings</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:34:59 +0000</pubDate>
      <link>https://dev.to/circuitsavage/the-cloudflare-5-second-challenge-explained-and-how-to-pass-it-2c21</link>
      <guid>https://dev.to/circuitsavage/the-cloudflare-5-second-challenge-explained-and-how-to-pass-it-2c21</guid>
      <description>&lt;p&gt;You request a page and Cloudflare hands you a spinner instead. "Checking your browser before you access the site." A few seconds later it either lets you through or loops you back to the spinner forever. That interstitial is the Cloudflare 5-second challenge, and whether you clear it comes down to what your client looks like, not how long you wait.&lt;/p&gt;

&lt;p&gt;The 5-second challenge (Cloudflare calls it a managed challenge, or the older JS challenge) is a full-page interstitial that runs JavaScript in your browser to score whether you're a real visitor. Pass it and Cloudflare sets a &lt;code&gt;cf_clearance&lt;/code&gt; cookie so it stops re-challenging you. Fail it and you never reach the actual site.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's different from Turnstile
&lt;/h2&gt;

&lt;p&gt;People mix these up constantly, so it's worth being precise. Turnstile is a &lt;em&gt;widget&lt;/em&gt; embedded in a form; it returns a &lt;em&gt;token&lt;/em&gt; you submit. The 5-second challenge is a &lt;em&gt;full-page gate&lt;/em&gt; in front of the whole site; it returns a &lt;em&gt;cookie&lt;/em&gt; you reuse on later requests. Different mechanism, different output, different handling.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Turnstile widget&lt;/th&gt;
&lt;th&gt;5-second challenge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it appears&lt;/td&gt;
&lt;td&gt;Inside a form (login, signup)&lt;/td&gt;
&lt;td&gt;Full page, before you see the site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it returns&lt;/td&gt;
&lt;td&gt;A cf-turnstile-response token&lt;/td&gt;
&lt;td&gt;A cf_clearance cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How you use it&lt;/td&gt;
&lt;td&gt;Submit the token with the form&lt;/td&gt;
&lt;td&gt;Send the cookie on subsequent requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;td&gt;Single-use, ~300s&lt;/td&gt;
&lt;td&gt;Session-length, tuned per site&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If it's the widget you're facing, go to &lt;a href="https://blog.peak.fo/what-is-cloudflare-turnstile-how-it-works/" rel="noopener noreferrer"&gt;what is Cloudflare Turnstile&lt;/a&gt; instead. This page is about the interstitial.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the challenge actually checks
&lt;/h2&gt;

&lt;p&gt;During those seconds, Cloudflare runs a JavaScript workload that probes your environment: the browser's fingerprint, whether APIs behave like a real browser, TLS characteristics of the connection, and the reputation of your IP. A plain HTTP client with no JavaScript engine can't run the workload at all, so it fails instantly. A headless browser can run it but often trips fingerprint checks. This is why raw &lt;code&gt;requests&lt;/code&gt; or &lt;code&gt;curl&lt;/code&gt; gets stuck in the loop no matter how many times you retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pass it
&lt;/h2&gt;

&lt;p&gt;Two options, same as with Turnstile. Drive a real browser hardened against detection, or call an API that returns the clearance. With Peak the &lt;code&gt;cloudflare5stask&lt;/code&gt; hands back the cookie, the matching user-agent, and the supporting headers in one response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;sol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.peak.fo/solve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk_your_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudflare5stask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@ip:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookies&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# keep using the same proxy IP that earned the clearance
&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target.com/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@ip:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one rule people forget: the clearance is bound to the &lt;strong&gt;IP and user-agent&lt;/strong&gt; that earned it. Reuse it from a different IP or with a different user-agent and Cloudflare throws it out. That's the whole reason to send it on a sticky proxy with the returned user-agent. The mechanics of reusing the cookie correctly are in &lt;a href="https://blog.peak.fo/the-cf_clearance-cookie-explained-and-how-to-reuse-it/" rel="noopener noreferrer"&gt;the cf_clearance cookie, explained&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retrying never helps
&lt;/h2&gt;

&lt;p&gt;The instinct when you hit the loop is to retry harder. It doesn't work, because nothing about the retry changes what Cloudflare is scoring. Same client, same fingerprint, same verdict. You either change what your client looks like or you get a valid clearance from something that can pass the check. Hammering the endpoint just gets your IP rate-limited faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Cloudflare 5-second challenge?
&lt;/h3&gt;

&lt;p&gt;It's a full-page interstitial that runs a JavaScript workload to decide whether you're a real browser. Pass it and Cloudflare sets a cf_clearance cookie so it stops challenging you. Fail and you can't reach the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do I keep getting the 5-second loop?
&lt;/h3&gt;

&lt;p&gt;Because your client can't pass the check, so Cloudflare never issues clearance. A plain HTTP client has no JavaScript engine; a headless browser often fails fingerprinting. Retrying with the same client changes nothing. Use a hardened browser or an API that returns the clearance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does clearance last?
&lt;/h3&gt;

&lt;p&gt;Cloudflare tunes it per site, so treat it as a session token: use it until a request gets challenged again, then refresh. Because Peak bills only on success, a refresh costs about a tenth of a cent and a miss costs nothing. See &lt;a href="https://peak.fo/#pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stuck in the 5-second loop? Grab a key free at&lt;/em&gt; &lt;a href="https://peak.fo/" rel="noopener noreferrer"&gt;&lt;em&gt;peak.fo&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to solve Cloudflare Turnstile in 2026 (a developer's guide)</title>
      <dc:creator>jujucodings</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:16:05 +0000</pubDate>
      <link>https://dev.to/circuitsavage/how-to-solve-cloudflare-turnstile-in-2026-a-developers-guide-44pc</link>
      <guid>https://dev.to/circuitsavage/how-to-solve-cloudflare-turnstile-in-2026-a-developers-guide-44pc</guid>
      <description>&lt;p&gt;If you scrape or automate anything, you've hit this wall. Requests that ran fine for months start coming back as 403s, or you get an HTML page that just says "Just a moment...". Open the URL in a real browser and there's a little Cloudflare box sitting on the form — Turnstile.&lt;/p&gt;

&lt;p&gt;This post covers three things: what Turnstile actually checks, why your headless script gets blocked no matter how many stealth patches you stack on it, and the handful of approaches that actually get you a token when you need one. There's runnable &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;playwright&lt;/code&gt; code at the end.&lt;/p&gt;

&lt;p&gt;Upfront disclosure: I work on &lt;a href="https://peak.fo" rel="noopener noreferrer"&gt;Peak&lt;/a&gt;, and one of the options below is our own solving API — I'll list the pricing and free tier plainly. But most of this is method and mechanics that cost nothing, and you can read only that part if you want. This isn't a pitch dressed up as a tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turnstile isn't a puzzle, it's a trust score
&lt;/h2&gt;

&lt;p&gt;Old-school CAPTCHAs — reCAPTCHA v2 with the fire hydrants, hCaptcha's "pick the buses" — test whether you can &lt;em&gt;solve a task&lt;/em&gt;. Turnstile mostly doesn't give you a task. It tests whether your browser &lt;em&gt;looks like a real person's browser&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what it does in the background:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs a chunk of obfuscated JS that collects a &lt;strong&gt;browser fingerprint&lt;/strong&gt; — navigator properties, screen, timezone, WebGL and Canvas rendering, fonts, and whether anything on &lt;code&gt;window&lt;/code&gt; smells like automation (&lt;code&gt;navigator.webdriver&lt;/code&gt;, CDP-injected objects, and so on).&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;consistency checks&lt;/strong&gt; on behavior and environment: mouse movement, the &lt;code&gt;isTrusted&lt;/code&gt; flag on events, execution timing, and whether the User-Agent you claim matches your real TLS and HTTP/2 fingerprint.&lt;/li&gt;
&lt;li&gt;Weighs the &lt;strong&gt;reputation of your exit IP&lt;/strong&gt;. Datacenter IPs — AWS, GCP, most VPS providers — start with a low score by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Score high enough and the widget fills in the &lt;code&gt;cf-turnstile-response&lt;/code&gt; token itself, fires its callback, and your form submits like normal. Score too low and you either get an interactive challenge or you simply never get a token.&lt;/p&gt;

&lt;p&gt;So the load-bearing idea: &lt;strong&gt;Turnstile isn't "solve one puzzle," it's "assemble one trustworthy browser session."&lt;/strong&gt; That single fact explains why every method below works or doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your headless script gets a 403
&lt;/h2&gt;

&lt;p&gt;Three reasons, biggest first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Datacenter IP.&lt;/strong&gt; This is the number-one killer. You're firing requests from a cloud box, the IP reputation is near the floor, and Turnstile cranks the difficulty. The exact same code from a home broadband (residential) IP often just passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless is loud.&lt;/strong&gt; A &lt;code&gt;headless=True&lt;/code&gt; Chromium has a pile of detectable tells. Even after &lt;code&gt;undetected-chromedriver&lt;/code&gt; or &lt;code&gt;playwright-stealth&lt;/code&gt;, the leftover CDP traces (&lt;code&gt;Runtime.enable&lt;/code&gt; and friends) still light up for Cloudflare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A plain HTTP client has no JS engine at all.&lt;/strong&gt; &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;curl_cffi&lt;/code&gt; can't run the challenge script Turnstile needs, so they never produce a token in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before you reach for a solver, the cheapest fix that people skip: &lt;strong&gt;a lot of the time you don't need to solve anything.&lt;/strong&gt; If the only reason you're being challenged is a datacenter IP, swap in a &lt;strong&gt;residential proxy&lt;/strong&gt; and a browser with a real fingerprint (Camoufox, or a properly stealth-patched Playwright), and the widget frequently passes on its own — &lt;code&gt;cf-turnstile-response&lt;/code&gt; fills itself in. Try that first. It's the lowest-cost path by a mile.&lt;/p&gt;

&lt;p&gt;To tell which wall you've hit, look at the response:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The page embeds a &lt;code&gt;&amp;lt;div class="cf-turnstile" data-sitekey="0x..."&amp;gt;&lt;/code&gt; &lt;strong&gt;form widget&lt;/strong&gt; → that's the kind this guide handles.&lt;/li&gt;
&lt;li&gt;The whole domain is behind a full-screen "Just a moment..." &lt;strong&gt;interstitial&lt;/strong&gt; (the managed challenge / "5-second" page) → that's a different beast. It runs on the &lt;code&gt;cf_clearance&lt;/code&gt; cookie, and I'll flag it separately at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: read the sitekey
&lt;/h2&gt;

&lt;p&gt;The sitekey is public. It's written into the HTML, starts with &lt;code&gt;0x&lt;/code&gt;, and every approach needs it first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target-site.com/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/128.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Most common: the data-sitekey attribute
&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data-sitekey=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'](0x[A-Za-z0-9_-]+)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sitekey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="c1"&gt;# Some sites render it in JS: turnstile.render(el, {sitekey: "0x..."})
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitekey[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']?\s*[:=]\s*[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'](0x[A-Za-z0-9_-]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sitekey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitekey:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's not in the HTML, the widget is being injected by JS. Open DevTools, filter the Network tab for &lt;code&gt;challenges.cloudflare.com&lt;/code&gt;, and you'll see the sitekey in the request params — or just run &lt;code&gt;document.querySelector('.cf-turnstile').dataset.sitekey&lt;/code&gt; in the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: get a valid token
&lt;/h2&gt;

&lt;p&gt;Minting the token yourself on a server — as covered above — mostly fails on datacenter IPs with automation tells. Two realistic paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path A — residential proxy + real browser, let the widget pass itself.&lt;/strong&gt; Good when volume is low and you can afford to spin up a browser. Use Camoufox (a Firefox fork with solid fingerprint spoofing) behind a residential proxy, and a lot of checkbox-style Turnstiles clear silently. The downside is it's slow and memory-hungry, and it doesn't scale once volume climbs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path B — call a solving API, get back just the token.&lt;/strong&gt; You send the &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;sitekey&lt;/code&gt; to a service, it runs the challenge on its own browser farm with residential IPs, and hands you back a &lt;code&gt;token&lt;/code&gt;. Your scraper stays pure HTTP the whole time — no browser. At scale this is basically the only sane option.&lt;/p&gt;

&lt;p&gt;Services that do this: 2Captcha, CapSolver, YesCaptcha, and &lt;a href="https://peak.fo/solve/cloudflare-turnstile/python/" rel="noopener noreferrer"&gt;Peak&lt;/a&gt;, where I work. The interfaces are all similar; here's Peak as the example (swap the URL and field names for another provider).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;solve_turnstile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turnstiletask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# keep the trailing slash
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitekey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&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="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;  &lt;span class="c1"&gt;# http://user:pass@ip:port
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.peak.fo/solve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solve failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;solve_turnstile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target-site.com/login/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real response looks like &lt;code&gt;{"success": true, "data": {"token": "0.abc123..."}, "cost": 0.0009}&lt;/code&gt; — on a live hard target one came back in a few seconds. You only get charged on &lt;code&gt;success&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One trap people hit constantly: &lt;strong&gt;if the token is going to be used behind a proxy afterward, the &lt;code&gt;proxy&lt;/code&gt; you pass at solve time should match the exit IP your later request uses.&lt;/strong&gt; Cloudflare sometimes binds the token to the IP that solved it. Mismatch the two and the server rejects the token as invalid even though it looks fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: use the token
&lt;/h2&gt;

&lt;p&gt;Once you have a token, there are two cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case one: pure HTTP, POST the token straight in
&lt;/h3&gt;

&lt;p&gt;The Turnstile token ends up as a single form field named &lt;code&gt;cf-turnstile-response&lt;/code&gt;. You POST it alongside your other form fields and you're done. A lot of login and submit endpoints are exactly this simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 ... Chrome/128.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target-site.com/login/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# the key field
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How do you find the field name? Open DevTools, submit the form once by hand, look at the form data on that POST in the Network tab, and copy it into your Python &lt;code&gt;data=&lt;/code&gt;. It isn't always literally &lt;code&gt;cf-turnstile-response&lt;/code&gt; — some sites rename it — so go by what you actually capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case two: inject the token in the browser and keep going
&lt;/h3&gt;

&lt;p&gt;If the target is a heavy single-page app and the submit logic lives in JS, put the token back into the page and manually fire Turnstile's callback so the frontend believes it passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inject_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(token) =&amp;gt; {
            // 1) fill the hidden response inputs
            document.querySelectorAll(
                &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input[name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;], input[name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;g-recaptcha-response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            ).forEach(el =&amp;gt; {
                el.value = token;
                el.dispatchEvent(new Event(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,  { bubbles: true }));
                el.dispatchEvent(new Event(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;change&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, { bubbles: true }));
            });
            // 2) fire the widget&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s success callback (many sites rely on this, not the hidden input)
            const el = document.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.cf-turnstile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;);
            const cb = el &amp;amp;&amp;amp; el.getAttribute(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data-callback&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;);
            if (cb &amp;amp;&amp;amp; typeof window[cb] === &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) {
                window[cb](token);
            }
        }&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://target-site.com/login/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sitekey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.cf-turnstile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-sitekey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;solve_turnstile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# reuse the function above
&lt;/span&gt;
    &lt;span class="nf"&gt;inject_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button[type=submit]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_load_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkidle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details decide whether this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data-callback&lt;/code&gt;.&lt;/strong&gt; Plenty of sites have no visible submit button — the moment verification passes, they move on, driven entirely by this callback. Fill the hidden input without firing the callback and nothing happens. The code above does both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dispatching &lt;code&gt;input&lt;/code&gt; / &lt;code&gt;change&lt;/code&gt;.&lt;/strong&gt; React and Vue don't notice a raw &lt;code&gt;.value&lt;/code&gt; assignment. You have to dispatch the events by hand so the framework sees the change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The traps worth memorizing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are single-use and short-lived.&lt;/strong&gt; Usually valid for a minute or two, once. Don't cache, don't reuse — solve and spend immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the trailing slash on the URL.&lt;/strong&gt; &lt;code&gt;https://x.com&lt;/code&gt; and &lt;code&gt;https://x.com/&lt;/code&gt; can validate differently in some implementations. Pass the exact URL your request actually hits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;action&lt;/code&gt; / &lt;code&gt;cData&lt;/code&gt; mismatch.&lt;/strong&gt; If the site passes an &lt;code&gt;action&lt;/code&gt; or &lt;code&gt;cData&lt;/code&gt; into &lt;code&gt;turnstile.render&lt;/code&gt;, you have to pass the matching value at solve time or the token won't validate. Both are findable in the HTML/JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full-screen "Just a moment" page is not the form widget.&lt;/strong&gt; That's the managed challenge (the "5-second shield"). It clears via the &lt;code&gt;cf_clearance&lt;/code&gt; cookie, not a &lt;code&gt;cf-turnstile-response&lt;/code&gt; token. Solving APIs usually have a separate endpoint for it (on Peak it's &lt;code&gt;cloudflare5stask&lt;/code&gt;, and it returns cookies, not a token). Different call, different params — don't mix them up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing an approach
&lt;/h2&gt;

&lt;p&gt;Running your own browser farm plus a residential proxy pool is fine at small volume, but the maintenance cost is higher than it looks once you scale — fingerprints have to keep pace with Cloudflare's changes, proxies rotate, failures need retries. So most people move to a ready-made API past a certain point. When you compare them, look at three things: &lt;strong&gt;billed per success or per request, residential proxy included or bring-your-own, and the real-world pass rate on Turnstile.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On price, roughly: 2Captcha's Turnstile runs about $1 per 1,000; CapSolver hovers around $0.80. For comparison, Peak's &lt;a href="https://peak.fo/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; is &lt;strong&gt;$0.90 per 1,000 successful Turnstile solves, dropping to $0.35 per 1,000 at volume, billed on success only — failures cost nothing&lt;/strong&gt;; a new account gets roughly 1,000 free solves to run the flow end-to-end first. The endpoint is the &lt;code&gt;POST https://api.peak.fo/solve&lt;/code&gt; from the code above.&lt;/p&gt;

&lt;p&gt;If you're on Playwright, Selenium, Scrapy, curl_cffi or Puppeteer and don't want to hand-roll the injection, there are ready MIT-licensed wrappers (on GitHub under &lt;code&gt;CircuitSavage&lt;/code&gt; — &lt;code&gt;playwright-turnstile&lt;/code&gt;, &lt;code&gt;selenium-turnstile&lt;/code&gt;, &lt;code&gt;scrapy-turnstile&lt;/code&gt;, &lt;code&gt;cloudscraper-turnstile&lt;/code&gt;, &lt;code&gt;turnstile-curl&lt;/code&gt;) that bundle the whole read-sitekey → call-API → inject-token loop. There are drop-ins for &lt;a href="https://peak.fo/solve/cloudflare-turnstile/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; and &lt;a href="https://peak.fo/solve/cloudflare-turnstile/go/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; too. Swap in an API key and go.&lt;/p&gt;

&lt;p&gt;One thing worth saying plainly: this is for legitimate automation — scraping public data, testing your own sites, QA. Respect the target's terms of service and robots rules, and don't point any of it at logins that aren't yours. Solving a challenge doesn't grant permission you didn't already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The right mental model for Turnstile isn't "solve a CAPTCHA," it's "assemble a trustworthy browser session." So the order is always:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First swap in a &lt;strong&gt;residential proxy + real-fingerprint browser&lt;/strong&gt; and let the widget pass on its own. This step is free and clears a large share of the datacenter-IP blocks.&lt;/li&gt;
&lt;li&gt;If it won't pass, or you need to scale, add a &lt;strong&gt;solving API for the token only&lt;/strong&gt; and keep the scraper pure HTTP.&lt;/li&gt;
&lt;li&gt;With the token in hand, either POST it straight into the &lt;code&gt;cf-turnstile-response&lt;/code&gt; field, or inject it in the browser and fire &lt;code&gt;data-callback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Watch the traps: single-use tokens, IP binding, the trailing slash, &lt;code&gt;action&lt;/code&gt; matching.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Understand what it's checking and you'll get further than by grinding on any single tool.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>cloudflare</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
