<?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: Bassem Shahin</title>
    <description>The latest articles on DEV Community by Bassem Shahin (@bshahin).</description>
    <link>https://dev.to/bshahin</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%2F3963470%2F74fdd403-aea6-477d-9330-39bd578dbbf4.png</url>
      <title>DEV Community: Bassem Shahin</title>
      <link>https://dev.to/bshahin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bshahin"/>
    <language>en</language>
    <item>
      <title>Build vs buy: what running your own CAPTCHA-solving stack actually costs</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Sat, 11 Jul 2026 20:46:18 +0000</pubDate>
      <link>https://dev.to/bshahin/build-vs-buy-what-running-your-own-captcha-solving-stack-actually-costs-4ae3</link>
      <guid>https://dev.to/bshahin/build-vs-buy-what-running-your-own-captcha-solving-stack-actually-costs-4ae3</guid>
      <description>&lt;h1&gt;
  
  
  Build vs buy: what running your own CAPTCHA-solving stack actually costs
&lt;/h1&gt;

&lt;p&gt;Every team that automates against the public web eventually hits the same fork: a scraper, a monitoring job, a QA suite, or an agent starts tripping CAPTCHAs, and someone asks &lt;em&gt;"should we just build our own solver instead of paying a service?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's a fair question. The per-solve prices you see quoted (a dollar or three per 1,000) &lt;em&gt;feel&lt;/em&gt; like something you could undercut with a weekend of code. Sometimes you can. But the sticker price of a solving service isn't really what you're comparing against — you're comparing against the &lt;strong&gt;fully-loaded cost of owning the problem forever.&lt;/strong&gt; That second number is the one people underestimate, so let me lay it out honestly, including when building genuinely is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "build your own" actually means
&lt;/h2&gt;

&lt;p&gt;The naive version — "I'll just click the checkbox with Playwright" — works in a demo and falls over in production. A real self-hosted solving stack for the modern challenge types (reCAPTCHA v2/v3/Enterprise, Cloudflare Turnstile) is not one script. It's a system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Challenge detection&lt;/strong&gt; — reliably identifying &lt;em&gt;which&lt;/em&gt; captcha is on a page and extracting its sitekey/parameters, across sites that change markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token generation&lt;/strong&gt; — a fleet of real (or hardened headless) browsers to actually produce valid tokens, because modern challenges are behavior- and signal-based, not image puzzles you can OCR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint + proxy management&lt;/strong&gt; — reCAPTCHA v3 and Turnstile score the &lt;em&gt;session&lt;/em&gt; (IP reputation, TLS/JA3, canvas, behavior), so you need residential/mobile egress and consistent fingerprints or your tokens come back low-score and useless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency + queueing&lt;/strong&gt; — running N solves in parallel without melting your box or getting the whole IP range flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry/scoring logic&lt;/strong&gt; — detecting a failed or low-score solve and retrying intelligently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; — success-rate dashboards, alerting when a vendor changes something and your success rate quietly craters overnight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is exotic. All of it is real engineering, and — this is the part that bites — &lt;strong&gt;none of it is done once.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden line item: the maintenance treadmill
&lt;/h2&gt;

&lt;p&gt;Here's the cost that never shows up in the weekend-project estimate. The anti-bot vendors ship changes continuously. Google rotates reCAPTCHA internals; Cloudflare iterates Turnstile; challenge parameters and scoring shift. Every one of those can silently drop your homegrown success rate, and you find out from a downstream failure, not a changelog.&lt;/p&gt;

&lt;p&gt;So "build your own" isn't a one-time build cost — it's a &lt;strong&gt;standing on-call commitment.&lt;/strong&gt; Someone on your team now owns keeping a captcha solver alive, and that someone is pulled off your actual product every time the arms race moves.&lt;/p&gt;

&lt;p&gt;Rough, honest math for a modest in-house stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Realistic cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial build (detection + browser fleet + proxy glue + retries)&lt;/td&gt;
&lt;td&gt;~1–3 engineer-months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxies (residential/mobile egress for valid scores)&lt;/td&gt;
&lt;td&gt;ongoing $$ — often more than the solver would've cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra (browser fleet, queue, monitoring)&lt;/td&gt;
&lt;td&gt;ongoing $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Maintenance&lt;/strong&gt; (the treadmill — reacting to vendor changes)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~0.2–0.5 of an engineer, indefinitely&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opportunity cost&lt;/td&gt;
&lt;td&gt;whatever that engineer &lt;em&gt;isn't&lt;/em&gt; building on your core product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At a loaded engineering cost, that maintenance fraction alone is often &lt;strong&gt;thousands of dollars a month&lt;/strong&gt; — before proxies and infra. Compare that to a solving subscription that's typically tens to low-hundreds of dollars a month, and the "cheap" homegrown option frequently isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  When building actually wins
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend buying always wins — it doesn't. Build when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solving &lt;em&gt;is&lt;/em&gt; your product.&lt;/strong&gt; If you're a proxy vendor, an anti-detect browser, or a scraping platform, captcha handling is core competency and differentiation. Own it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have genuinely unique requirements&lt;/strong&gt; a general service can't meet — an obscure challenge type, an air-gapped environment, strict data-residency rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extreme volume with margin pressure&lt;/strong&gt; where, at your scale, an in-house team is demonstrably cheaper than any per-use or per-thread pricing — and you can staff the treadmill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of those is you, building is rational. Budget for the maintenance, not just the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  When buying wins (most teams)
&lt;/h2&gt;

&lt;p&gt;For everyone else — teams where scraping/automation is a &lt;em&gt;means&lt;/em&gt;, not the product — buying is almost always the right call, for one reason: &lt;strong&gt;you're offloading the treadmill, not just the code.&lt;/strong&gt; A solving service absorbs every vendor change so your engineers stay on your roadmap. You wire up an API once and stop thinking about it.&lt;/p&gt;

&lt;p&gt;And the integration is genuinely small. Most services expose the same three-step flow — submit the sitekey, poll for the token, inject it — and many are 2Captcha-API-compatible, so if you've ever used one you've basically used all of them:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE&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://ocr.captchaai.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 2Captcha-compatible endpoint
&lt;/span&gt;
&lt;span class="c1"&gt;# 1) submit the challenge (reCAPTCHA v2 shown; Turnstile = method="turnstile" + sitekey)
&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;get&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/in.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;method&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;userrecaptcha&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;googlekey&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;SITE_KEY_FROM_THE_PAGE&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;pageurl&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.example/login&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;task_id&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 2) poll for the token
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;res&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAPCHA_NOT_READY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&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;res&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="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="c1"&gt;# 3) inject the token into the page and submit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration surface. No browser fleet, no proxy sourcing for score quality, no 2 a.m. page when a vendor ships a change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually decide
&lt;/h2&gt;

&lt;p&gt;Skip the per-1,000 sticker comparison. Ask three questions instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is captcha solving part of what we sell?&lt;/strong&gt; If yes, lean build. If no, lean buy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can we staff the maintenance treadmill indefinitely?&lt;/strong&gt; Not the build — the &lt;em&gt;upkeep&lt;/em&gt;. If that makes you wince, buy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the fully-loaded in-house number&lt;/strong&gt; — build + proxies + infra + that 0.2–0.5 engineer forever + opportunity cost — versus a subscription? Run &lt;em&gt;that&lt;/em&gt; comparison, not the sticker one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most teams the honest answer is: your engineers are worth more on your product than on an arms race you'll never win outright. Buy the boring part, ship your actual thing.&lt;/p&gt;

&lt;p&gt;If you want to sanity-check the buy side against your real volume, &lt;a href="https://captchaai.com/lp/cost-calculator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=build-vs-buy" rel="noopener noreferrer"&gt;CaptchaAI's cost calculator&lt;/a&gt; lets you plug in your numbers, and there's a &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=build-vs-buy" rel="noopener noreferrer"&gt;free trial&lt;/a&gt; if you want to compare success rates against your homegrown prototype before deciding. But whichever way you go, decide on the fully-loaded number — not the weekend-project fantasy.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>automation</category>
      <category>captcha</category>
      <category>engineering</category>
    </item>
    <item>
      <title>reCAPTCHA v2 vs v3 vs Enterprise — how to tell which one you're fighting (and how to solve each)</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Tue, 30 Jun 2026 23:17:04 +0000</pubDate>
      <link>https://dev.to/bshahin/recaptcha-v2-vs-v3-vs-enterprise-how-to-tell-which-one-youre-fighting-and-how-to-solve-each-4bpo</link>
      <guid>https://dev.to/bshahin/recaptcha-v2-vs-v3-vs-enterprise-how-to-tell-which-one-youre-fighting-and-how-to-solve-each-4bpo</guid>
      <description>&lt;h1&gt;
  
  
  reCAPTCHA v2 vs v3 vs Enterprise — how to tell which one you're fighting
&lt;/h1&gt;

&lt;p&gt;"reCAPTCHA is blocking me" is the start of a debugging session, not the end of one. There are four practical variants — &lt;strong&gt;v2 checkbox&lt;/strong&gt;, &lt;strong&gt;v2 invisible&lt;/strong&gt;, &lt;strong&gt;v3 score&lt;/strong&gt;, and &lt;strong&gt;Enterprise&lt;/strong&gt; — and they behave completely differently. They fail differently, they're solved differently, and the call you make to a solver isn't the same shape. So before you touch any code, the first job is identifying which one you're up against. The page tells you, if you know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell which variant it is
&lt;/h2&gt;

&lt;p&gt;Everything you need is in the page source and the script tags. Open devtools, look at the rendered widget and the api.js include.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v2 checkbox&lt;/strong&gt; — the classic "I'm not a robot" box. In the DOM you'll see a &lt;/p&gt;, and the page loads &lt;a href="https://www.google.com/recaptcha/api.js" rel="noopener noreferrer"&gt;https://www.google.com/recaptcha/api.js&lt;/a&gt;. The sitekey starts with 6L. There's a visible checkbox; clicking it sometimes triggers an image grid ("select all bicycles").

&lt;p&gt;&lt;strong&gt;v2 invisible&lt;/strong&gt; — same api.js, same 6L... sitekey shape, but &lt;strong&gt;no visible checkbox&lt;/strong&gt;. The widget is bound to a button or fired via grecaptcha.execute(), often with data-size="invisible". It only shows an image challenge if Google decides the session looks risky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v3 score&lt;/strong&gt; — there is &lt;strong&gt;no widget and no checkbox at all&lt;/strong&gt;. The script include is &lt;a href="https://www.google.com/recaptcha/api.js?render=" rel="noopener noreferrer"&gt;https://www.google.com/recaptcha/api.js?render=&lt;/a&gt; — the ?render= parameter is the dead giveaway. The page calls grecaptcha.execute(sitekey, {action: 'login'}) and gets back a token plus a &lt;strong&gt;0.0–1.0 score&lt;/strong&gt; that the site's backend evaluates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise&lt;/strong&gt; — the script is &lt;a href="https://www.google.com/recaptcha/enterprise.js?render=" rel="noopener noreferrer"&gt;https://www.google.com/recaptcha/enterprise.js?render=&lt;/a&gt;... (note enterprise.js, not api.js), and the JS calls live under grecaptcha.enterprise.*. Enterprise can run in a v2-style or v3-style mode, so identify it by the enterprise.js include first, then check widget vs. ?render=.&lt;/p&gt;

&lt;p&gt;Quick reference for the script tell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;api.js + visible checkbox → &lt;strong&gt;v2 checkbox&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;api.js + data-size="invisible" / execute() → &lt;strong&gt;v2 invisible&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;api.js?render=KEY → &lt;strong&gt;v3&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;enterprise.js → &lt;strong&gt;Enterprise&lt;/strong&gt; (then check widget vs. score mode)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What each means for automation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v2 checkbox&lt;/strong&gt; is a puzzle problem — you need a token representing a solved challenge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v2 invisible&lt;/strong&gt; is mostly the same under the hood, except the challenge only surfaces when you look risky. Clean up IP/fingerprint and it often passes silently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v3&lt;/strong&gt; is a reputation problem, not a puzzle. Nothing to click — you're scored on IP, fingerprint, behavior, and the action. A "fail" means you scored below the site's threshold.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enterprise&lt;/strong&gt; is the same two modes wearing a different SDK. The practical difference is the namespace and sometimes a required actionenterprise flag. It's the same v2/v3 mechanics with more backend signals on the site's side; your client job barely changes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common mistake: grabbing a token for the wrong variant — a plain v2 token sent to a v3 endpoint, or v3 solved without the matching action. The token comes back fine, the form submits, and the site silently rejects it. Identifying correctly up front saves that whole loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solve flow per variant
&lt;/h2&gt;

&lt;p&gt;If your solving already speaks the 2Captcha-style API method=userrecaptcha, googlekey, pageurl), every variant is the same endpoint with a couple of parameters flipped. Submit to in.php, poll res.php, drop the token into g-recaptcha-response.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v2 checkbox / invisible:&lt;/strong&gt; method=userrecaptcha, googlekey=, pageurl=. Invisible uses the exact same call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v3:&lt;/strong&gt; add version=v3 and action= (e.g. login). The action must match what the page passes to execute(). You can't request a score — the &lt;strong&gt;site's backend sets the minimum&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; add enterprise=1 alongside the v2 or v3 parameters depending on the mode. Same googlekeypageurl; if score mode, still send action.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the v3 case end to end; v2 is the same minus versionaction, Enterprise adds enterprise=1:&lt;br&gt;
&lt;/p&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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="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;6Lc...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                       &lt;span class="c1"&gt;# the data-sitekey / render key
&lt;/span&gt;
&lt;span class="n"&gt;PAGEURL&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://target.example/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# submit (v3 shown; drop version+action for v2, add enterprise=1 for Enterprise)
&lt;/span&gt;
&lt;span class="n"&gt;rid&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://ocr.captchaai.com/in.php&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;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;method&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;userrecaptcha&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;version&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;v3&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;actison&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;login&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;googlekey&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageurl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PAGEURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# poll
&lt;/span&gt;
&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;res&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://ocr.captchaai.com/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="mi"&gt;1&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;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="c1"&gt;# submit token as g-recaptcha-response (matching the action for v3)
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Identify from the &lt;strong&gt;script include&lt;/strong&gt;: api.js (v2), api.js?render= (v3), enterprise.js (Enterprise); then checkbox vs. invisible vs. score by the widget/flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;v2 = puzzle/token&lt;/strong&gt;, &lt;strong&gt;v3 = reputation/score&lt;/strong&gt;, &lt;strong&gt;Enterprise = same two modes, different SDK + an enterprise flag&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Solve flow is one endpoint: userrecaptcha + googlekey + pageurl, with version=v3action for v3 and enterprise=1 for Enterprise.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;CaptchaAI returns tokens for reCAPTCHA v2 (checkbox + invisible), v3 (with action), and Enterprise, and it's 2Captcha-API-compatible — so an existing client is mostly a base-URL change. &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=recaptcha-v2-v3-enterprise-how-to-tell-and-solve" rel="noopener noreferrer"&gt;The trial is free&lt;/a&gt; (3 days, no card).&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
      <category>captcha</category>
    </item>
    <item>
      <title>Why your Cloudflare Turnstile token works in the browser but 403s from requests</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:54:14 +0000</pubDate>
      <link>https://dev.to/bshahin/why-your-cloudflare-turnstile-token-works-in-the-browser-but-403s-from-requests-3hli</link>
      <guid>https://dev.to/bshahin/why-your-cloudflare-turnstile-token-works-in-the-browser-but-403s-from-requests-3hli</guid>
      <description>&lt;h1&gt;
  
  
  Why your Cloudflare Turnstile token works in the browser but 403s from requests
&lt;/h1&gt;

&lt;p&gt;You solved the Turnstile widget. You can see the token in the page. You copy it into your script, POST the form from requests, and the server hands you back a 403 — or a JSON body with "success": false. The token clearly worked a second ago in the browser, so what changed?&lt;/p&gt;

&lt;p&gt;Short answer: a Turnstile token is not a password you can carry around. It's a one-time, short-lived proof bound to a very specific context, and replaying it from a different context is exactly what it's designed to reject. Below is what that context is, how to tell which constraint you're hitting, and the fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real scenario
&lt;/h2&gt;

&lt;p&gt;You're automating a flow on a Cloudflare-protected site. There's a cf-turnstile widget on the form. You get a token one of two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;you render the page in a real browser (Playwright/Selenium) and read cf-turnstile-response, or&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you hand the sitekey + page URL to a solving service and get a token back.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, you then submit the form with a plain HTTP client requests, httpx, axios) and it fails. The frustrating part: it's intermittent-looking. The reason it feels random is that there are four separate constraints, and you're usually tripping a different one each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things a Turnstile token is bound to
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It's single-use
&lt;/h3&gt;

&lt;p&gt;Once Cloudflare validates a token server-side (the siteverify call your target makes), that token is spent. Submit twice, retry, or test it once by hand, and the second use returns false. You get a fresh one per submission.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It has a short TTL
&lt;/h3&gt;

&lt;p&gt;Turnstile tokens expire fast — a few minutes. Solve early, do other work, submit later, and the token can be dead on arrival. The widget auto-refreshes in the browser precisely because tokens go stale; a script that grabs the token and sits on it loses that refresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It's bound to the sitekey and the page URL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multiple widgets. Some pages embed more than one Turnstile (login + newsletter). Solving the wrong sitekey gives a token the server rejects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runtime-injected sitekeys. Many sites inject the sitekey with JS at render time, sometimes rotating it. If you scraped it from static HTML once, it may already be wrong. Read it off the rendered widget.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. If it's a managed challenge, the browser gets re-checked on submit
&lt;/h3&gt;

&lt;p&gt;This one bites people who "did everything right." A standalone widget issues a token you can submit from anywhere. But Cloudflare's managed challenge re-evaluates the request on submission: TLS/JA3 fingerprint, the cf_clearance cookie, IP reputation. A token minted in a real browser, then replayed from a raw HTTP client with a different fingerprint and no clearance cookie, fails that second check no matter how valid the token is.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell which one you're hitting
&lt;/h2&gt;

&lt;p&gt;Don't guess — read the 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;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="n"&gt;target_url&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;form&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="n"&gt;headers&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="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;headers&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;cf-mitigated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# present =&amp;gt; Cloudflare challenge layer
&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;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                     &lt;span class="c1"&gt;# body usually names the cause
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;JSON {"success": false, "error-codes": [...]} from the site's verify endpoint → it's the token (expired/reused/wrong sitekey). Codes are explicit: timeout-or-duplicate, invalid-input-response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 403 with a cf-mitigated header + challenge-page body → it's the fingerprint/clearance layer (#4), not the token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 403503 with a Cloudflare code like 1020 → a WAF/IP decision; no token solves that.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;For a standalone widget (causes 1–3): solve against the exact sitekey from the rendered widget and the exact page URL; submit immediately (seconds, not minutes); solve once per submission, never reuse. Token flow with a 2Captcha-compatible API (so an existing 2Captcha client is a base-URL change):&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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="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;0x4AAAAA...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# read from the rendered widget, not static HTML
&lt;/span&gt;
&lt;span class="n"&gt;PAGEURL&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://target.example/login&lt;/span&gt;&lt;span class="sh"&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;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://ocr.captchaai.com/in.php&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;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;method&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;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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageurl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PAGEURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;task_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&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="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;res&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://ocr.captchaai.com/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="mi"&gt;1&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;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="n"&gt;form&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;   &lt;span class="c1"&gt;# submit RIGHT AWAY — single-use, short-lived
&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="n"&gt;PAGEURL&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;form&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="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a managed challenge (cause 4): a bare token isn't enough — carry the browser context it was minted in. Keep the cf_clearance cookie; pin the same IP, same User-Agent, and a browser-matching TLS fingerprint from minting through submission (e.g. curl_cffi with impersonate, or submit in the same browser context). Rotating the proxy or switching to a raw-client UA breaks it.&lt;/p&gt;

&lt;p&gt;Rule of thumb: match the token to its sitekey+URL, submit before it ages, and keep the fingerprint that solved a managed challenge identical through submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick gut-check
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] Sitekey read from the rendered widget (handles injection / multiple widgets)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] Page URL passed to the solve matches the page you submit to&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] Token submitted within seconds, used exactly once&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] Checked the response for cf-mitigated / Cloudflare codes (token-layer vs fingerprint-layer)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] Managed challenge: same IP + UA + TLS fingerprint, clearance cookie carried through&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the response first. Nine times out of ten it tells you whether you're fighting the token or the fingerprint — and those are very different fixes.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>cloudflare</category>
      <category>captcha</category>
    </item>
    <item>
      <title>Why your reCAPTCHA v3 score is low — and how to actually raise it</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Thu, 25 Jun 2026 08:14:51 +0000</pubDate>
      <link>https://dev.to/bshahin/why-your-recaptcha-v3-score-is-low-and-how-to-actually-raise-it-5554</link>
      <guid>https://dev.to/bshahin/why-your-recaptcha-v3-score-is-low-and-how-to-actually-raise-it-5554</guid>
      <description>&lt;h1&gt;
  
  
  Why your reCAPTCHA v3 score is low — and how to actually raise it
&lt;/h1&gt;

&lt;p&gt;reCAPTCHA v3 is the one that never shows a checkbox or a puzzle. Instead it watches the whole session and hands the site a &lt;strong&gt;score from 0.0 to 1.0&lt;/strong&gt; — roughly "how human does this look." The site then decides what to do with it (allow, step up, block) based on its own threshold. So when your automation "fails reCAPTCHA v3," there's nothing to click — you're just scoring too low. The fix is understanding what it scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v3 is actually scoring
&lt;/h2&gt;

&lt;p&gt;v3 doesn't test whether you can solve something. It builds a &lt;strong&gt;risk score&lt;/strong&gt; from signals across the page load and session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IP reputation&lt;/strong&gt; — datacenter ranges score low almost by default; residential/mobile score higher.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Browser fingerprint&lt;/strong&gt; — navigator.webdriver, headless/automation tells, missing or inconsistent client-hints, a TLS/JA3 that doesn't match the UA you claim.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Behavior + history&lt;/strong&gt; — mouse movement, timing, whether you have Google cookies / a browsing history, how you arrived at the page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The action parameter&lt;/strong&gt; — v3 tags each execution with an action (e.g. login); a mismatch or a generic action looks off.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key mental model: &lt;strong&gt;a low score is a "you look automated" verdict, assembled before you ever submit.&lt;/strong&gt; There's no token to "solve" your way past it — you raise the score or you produce a token that already scores high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why yours is low (the usual suspects)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Running from a &lt;strong&gt;datacenter IP&lt;/strong&gt; (AWS/GCP/cloud) — the single biggest score killer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Default Selenium/Playwright&lt;/strong&gt; — leaks navigator.webdriver + CDP/headless artifacts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;cold session&lt;/strong&gt; — no cookies, no history, straight to the protected action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Machine-speed behavior&lt;/strong&gt; — instant navigation/clicks, no human-ish timing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fingerprint mismatch&lt;/strong&gt; — a Chrome UA over a Pythonrequests TLS fingerprint, or a geo/timezone that disagrees with the IP.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to actually raise it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix the IP first&lt;/strong&gt; — residential/mobile, geo-consistent with the rest of the profile. This alone moves the score the most.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kill the automation tells&lt;/strong&gt; — a stealth/patched browser (or a real one) so navigator.webdriver is false, client-hints are consistent, and the TLS fingerprint matches the UA.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Warm the session&lt;/strong&gt; — keep cookies, do a little real navigation before the scored action, don't jump straight to the endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Human-ish behavior&lt;/strong&gt; — realistic timing and interaction, not perfect machine cadence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the right action&lt;/strong&gt; — match the action the site expects for that step.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For automation that still scores too low after the fundamentals, the practical route is to &lt;strong&gt;fetch a token from a solving service&lt;/strong&gt; that produces high-scoring v3 tokens, then submit it. One important nuance people miss: &lt;strong&gt;the minimum score is set by the target site, not by you or the solver&lt;/strong&gt; — so you can't "request 0.9"; you produce the best token possible and the site's threshold decides. Getting the fundamentals right is what makes that token land above their cutoff.&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&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;PAGEURL&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6Lc...&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.example/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;rid&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://ocr.captchaai.com/in.php&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;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;method&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;userrecaptcha&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;version&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;v3&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;action&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;login&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;googlekey&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageurl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PAGEURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;request&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="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;res&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://ocr.captchaai.com/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="mi"&gt;1&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;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="c1"&gt;# submit token as g-recaptcha-response for the matching action
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;v3 gives a &lt;strong&gt;0.0–1.0 score&lt;/strong&gt;, not a puzzle — a low score means "looks automated," assembled from IP + fingerprint + behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Biggest levers, in order: &lt;strong&gt;residential geo-matched IP → no automation fingerprint → warm session → human-ish behavior → correct action.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;site sets the min-score threshold&lt;/strong&gt;, not you — so fix the fundamentals; a token only lands if the rest looks legit.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want to test the token flow against your own target, CaptchaAI returns reCAPTCHA v3 tokens (with action) and is 2Captcha-API-compatible, so an existing client is mostly a base-URL change — &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=why-recaptcha-v3-score-low-how-to-raise-it" rel="noopener noreferrer"&gt;the trial is free&lt;/a&gt; (3 days, no card).&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
      <category>captcha</category>
    </item>
    <item>
      <title>Migrating reCAPTCHA solving off 2Captcha: the one-line swap (and what changes at volume)</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Wed, 24 Jun 2026 08:56:41 +0000</pubDate>
      <link>https://dev.to/bshahin/migrating-recaptcha-solving-off-2captcha-the-one-line-swap-and-what-changes-at-volume-41pe</link>
      <guid>https://dev.to/bshahin/migrating-recaptcha-solving-off-2captcha-the-one-line-swap-and-what-changes-at-volume-41pe</guid>
      <description>&lt;h1&gt;
  
  
  Migrating reCAPTCHA solving off 2Captcha: the one-line swap
&lt;/h1&gt;

&lt;p&gt;Most teams who solve reCAPTCHA at volume started on 2Captcha. What people don't realize is that the 2Captcha API has effectively become a shared interface — several providers implement the same in.php / res.php contract. So switching usually isn't a rewrite; it's a base-URL change.&lt;/p&gt;

&lt;p&gt;This matters because the main reason to switch is cost at volume (per-1,000 scales linearly; thread-based flattens it). If migrating were a big refactor the savings wouldn't be worth it. It isn't, so they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual diff
&lt;/h2&gt;

&lt;p&gt;Typical 2Captcha reCAPTCHA 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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;BASE&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://2captcha.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# the only provider-specific line
&lt;/span&gt;
&lt;span class="n"&gt;submit&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/in.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;method&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;userrecaptcha&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;googlekey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SITE_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;pageurl&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_URL&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;submit&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="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAPCHA_NOT_READY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&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;r&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="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrating to a 2Captcha-compatible 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="n"&gt;BASE&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://ocr.captchaai.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# changed
&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_NEW_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;               &lt;span class="c1"&gt;# changed
&lt;/span&gt;
&lt;span class="c1"&gt;# everything else is byte-for-byte identical
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;method=userrecaptcha, googlekey, pageurl, the OK| response, CAPCHA_NOT_READY polling, the token — all the same. Wrapped it in a client class? Change one constant. Used an SDK? Point it at the new base URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays identical
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The reCAPTCHA flow: v2 checkbox, v2 invisible, v3 version=v3 + min_score) — same call shape.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token handling, your retry/timeout/polling logic — untouched.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What changes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The pricing model (the point): per-1,000 → per-&lt;em&gt;thread&lt;/em&gt; with unlimited solves. Size for peak concurrency, not monthly total.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latency / success-rate are provider-specific — so measure, don't assume.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to migrate without betting the pipeline on it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Keep 2Captcha wired in; add the new provider behind the same interface (same calls — trivial).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Route ~10% of traffic to the new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compare on your targets: success rate, median latency, cost per 1,000.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Holds up? Ramp. Doesn't? You've lost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the API is identical, that A/B harness is a few lines and a flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version
&lt;/h2&gt;

&lt;p&gt;A compatible provider is a drop-in at the API level — that part is a one-line swap. Whether it's the right swap depends on your numbers: run the A/B on real traffic and let the data decide. Compatibility just makes that test cheap.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
      <category>captcha</category>
    </item>
    <item>
      <title>The real cost of solving reCAPTCHA at scale (per-1,000 vs thread-based)</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:23:28 +0000</pubDate>
      <link>https://dev.to/bshahin/the-real-cost-of-solving-recaptcha-at-scale-per-1000-vs-thread-based-379j</link>
      <guid>https://dev.to/bshahin/the-real-cost-of-solving-recaptcha-at-scale-per-1000-vs-thread-based-379j</guid>
      <description>&lt;h1&gt;
  
  
  The real cost of solving reCAPTCHA at scale
&lt;/h1&gt;

&lt;p&gt;If you automate anything on the public web for long enough, reCAPTCHA is the wall you hit most. It's on far more sites than Turnstile, hCaptcha, or the enterprise bot vendors combined. So when you wire in a solving service, the interesting question usually isn't "can it solve reCAPTCHA" (most can). It's what does it cost when you're doing this 100,000 times a month — or 10 million?&lt;/p&gt;

&lt;p&gt;That's where the pricing model matters more than the per-solve price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways solvers bill you
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Per-1,000 solves (usage-based). 2Captcha, Anti-Captcha, CapSolver, CapMonster — most of the market — charge per solve, quoted per 1,000 (~$1–$3 for reCAPTCHA). Your bill scales linearly with volume. Double the traffic, double the cost. Forever.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Thread-based (concurrency-based). A "thread" is one concurrent in-flight solve. You buy N threads and get unlimited solves per thread per month. Cost scales with peak concurrency, not total volume — so once sized, pushing more solves through is free.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The math
&lt;/h2&gt;

&lt;p&gt;reCAPTCHA v2 at ~$2 per 1,000 vs thread-based tiers (illustrative — check current pricing):&lt;/p&gt;

&lt;p&gt;| Monthly solves | Per-1,000 (~$2/1k) | Thread plan | Thread cost | Effective per-1k |&lt;/p&gt;

&lt;p&gt;|---|---|---|---|---|&lt;/p&gt;

&lt;p&gt;| 10,000 | ~$20 | 5 threads | ~$15 | ~$1.50 |&lt;/p&gt;

&lt;p&gt;| 100,000 | ~$200 | 5 threads | ~$15 | ~$0.15 |&lt;/p&gt;

&lt;p&gt;| 1,000,000 | ~$2,000 | 50 threads | ~$90 | ~$0.09 |&lt;/p&gt;

&lt;p&gt;| 10,000,000 | ~$20,000 | 200 threads | ~$300 | ~$0.015 |&lt;/p&gt;

&lt;p&gt;The usage column grows in a straight line. The thread column barely moves. At a million/month the effective per-1,000 is single-digit cents; at ten million it's a rounding error.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Low/bursty volume → usage-based (or a free tier). A few thousand a month with idle gaps? Pay per solve; you're not paying for concurrency you don't use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sustained/high volume → thread-based. Solving continuously? Flat per-thread wins, and the gap widens the more you push.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one ask of thread-based: size threads to peak concurrency, not total volume. Watch your live concurrency for a day, buy ~that many, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token flow (so this isn't just pricing)
&lt;/h2&gt;

&lt;p&gt;reCAPTCHA v2/v3 is the same three steps regardless of vendor — and on a 2Captcha-compatible API it's identical calls:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;BASE&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://ocr.captchaai.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# 2Captcha-compatible
&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;get&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/in.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;method&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;userrecaptcha&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;googlekey&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;SITE_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;pageurl&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.example/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;task_id&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;res&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAPCHA_NOT_READY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&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;res&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="nf"&gt;split&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="c1"&gt;# inject token into g-recaptcha-response, submit the form
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For v3 the same flow returns a token, but v3 returns a score driven by the session's reputation, not a puzzle — a separate rabbit hole; the cost model is the same either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;For reCAPTCHA, don't ask the headline per-1,000 rate — ask what your bill looks like at 10× your volume. Usage-based 10×'s with you; thread-based is roughly flat. Size threads to peak concurrency, run the math on your real volume, and pick the model that matches your curve.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
      <category>captcha</category>
    </item>
    <item>
      <title>What DataDome actually checks — and why your Cloudflare playbook doesn’t transfer</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 13:54:13 +0000</pubDate>
      <link>https://dev.to/bshahin/what-datadome-actually-checks-and-why-your-cloudflare-playbook-doesnt-transfer-24b5</link>
      <guid>https://dev.to/bshahin/what-datadome-actually-checks-and-why-your-cloudflare-playbook-doesnt-transfer-24b5</guid>
      <description>&lt;h1&gt;
  
  
  What DataDome actually checks — and why your Cloudflare playbook doesn’t transfer
&lt;/h1&gt;

&lt;p&gt;You’ve got your Cloudflare setup dialed in — clean residential IPs, a real browser, a solver for the Turnstile widget — and then you hit a site that blocks you anyway, with a challenge that looks nothing like Cloudflare’s. Odds are it’s &lt;strong&gt;DataDome&lt;/strong&gt;, and the reason your playbook stops working is that DataDome isn’t primarily a captcha. It’s a request-analysis and device-fingerprinting engine that &lt;em&gt;sometimes&lt;/em&gt; shows a captcha. Treat it like Cloudflare and you’ll spin.&lt;/p&gt;

&lt;p&gt;Here's how to recognize it, what it's really doing, and how to approach it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to know it’s DataDome (not Cloudflare)
&lt;/h2&gt;

&lt;p&gt;Read the response — the markers are distinct:&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;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;get&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;headers&lt;/span&gt;&lt;span class="o"&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;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="c1"&gt;# often 403
&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;datadome&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;r&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;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;set-cookie&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;# a `datadome` cookie
&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;headers&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;x-datadome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;headers&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;x-dd-b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# DataDome response headers
&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;captcha-delivery.com&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;r&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;# DataDome's challenge/captcha host
&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;dd&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;r&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;# a {"dd": {...}} JSON block in the 403 body
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;datadome&lt;/code&gt; cookie&lt;/strong&gt;, an &lt;strong&gt;&lt;code&gt;x-datadome&lt;/code&gt;&lt;/strong&gt; header, references to &lt;strong&gt;&lt;code&gt;captcha-delivery.com&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;ct.captcha-delivery.com&lt;/code&gt;&lt;/strong&gt;, or a &lt;code&gt;{"dd": {...}}&lt;/code&gt; JSON block in a &lt;code&gt;403&lt;/code&gt; → that’s DataDome.&lt;/li&gt;
&lt;li&gt;Cloudflare’s tells are different: &lt;code&gt;cf-mitigated&lt;/code&gt;, &lt;code&gt;cdn-cgi/challenge-platform&lt;/code&gt;, &lt;code&gt;cf_clearance&lt;/code&gt;. If you see those, it’s Cloudflare, and a different guide applies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting the vendor right is the whole first step, because the fix is not the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it’s harder than “solve the captcha”
&lt;/h2&gt;

&lt;p&gt;DataDome decides whether to even &lt;em&gt;show&lt;/em&gt; a captcha based on a risk score it builds from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request fingerprint&lt;/strong&gt; — TLS/JA3, HTTP/2 settings, header order. A raw client (or an obvious headless browser) scores badly before any challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device signals&lt;/strong&gt; — a heavy client-side JS payload reads canvas/WebGL/fonts/timing/sensors; inconsistencies (a “desktop Chrome” with mobile-ish signals, or a timezone that disagrees with the IP) flag instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior&lt;/strong&gt; — mouse movement, timing, navigation patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the captcha is the &lt;em&gt;last&lt;/em&gt; gate, not the first. This is why people who “solve the DataDome captcha” still get blocked on the next request: the captcha token doesn’t fix a fingerprint that already looks automated. The defense is mostly upstream of the challenge.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm it’s DataDome first&lt;/strong&gt; (above) — don’t apply Cloudflare clearance logic to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for a way around the wall.&lt;/strong&gt; Often the cheapest win: check whether the site exposes the same data via an official API or a backing JSON endpoint the page itself calls (devtools → Network → XHR). That can skip DataDome entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you must go through the front, fix the layers in order:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP&lt;/strong&gt; — residential/mobile, geo-matched to your profile. Datacenter IPs score poorly with DataDome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint&lt;/strong&gt; — a real or patched-stealth browser whose values are &lt;em&gt;internally consistent&lt;/em&gt; (and consistent with the IP’s geo). DataDome is stricter than Cloudflare on automation tells, so default Selenium/Playwright usually won’t survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior&lt;/strong&gt; — human-ish timing and interaction, not machine-perfect.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interactive captcha, when it appears&lt;/strong&gt; (DataDome’s image/slider), is solvable like other image/interactive captchas — but only &lt;em&gt;after&lt;/em&gt; the fingerprint/behavior look legitimate; otherwise a solved challenge just bounces on the next request. And the &lt;code&gt;datadome&lt;/code&gt; cookie you earn is bound to the IP + UA + fingerprint that earned it (much like &lt;code&gt;cf_clearance&lt;/code&gt;), so pin the session and don’t rotate mid-flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest summary: DataDome is a fingerprint/behavior problem with a captcha on top, not a captcha problem. Spend your effort on the IP + fingerprint + behavior stack; the challenge is the easy 20%.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identify by the response&lt;/strong&gt;: &lt;code&gt;datadome&lt;/code&gt; cookie / &lt;code&gt;x-datadome&lt;/code&gt; header / &lt;code&gt;captcha-delivery.com&lt;/code&gt; / &lt;code&gt;{"dd":...}&lt;/code&gt; 403 → DataDome (not Cloudflare).&lt;/li&gt;
&lt;li&gt;It scores &lt;strong&gt;fingerprint + device + behavior&lt;/strong&gt; before showing a captcha, so a solved challenge alone won’t save a bad fingerprint.&lt;/li&gt;
&lt;li&gt;Order of fixes: residential geo-matched IP → internally-consistent real/stealth browser → human-ish behavior → (only then) the interactive challenge.&lt;/li&gt;
&lt;li&gt;Pin the session — the &lt;code&gt;datadome&lt;/code&gt; cookie is bound to IP+UA+fingerprint.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;For the everyday captcha types you hit alongside this — reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, image — CaptchaAI is 2Captcha-API-compatible, so an existing client is mostly a base-URL change, and &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=what-datadome-checks-and-how-to-handle-it" rel="noopener noreferrer"&gt;the trial is free&lt;/a&gt; (3 days, no card).&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>datadome</category>
      <category>automation</category>
    </item>
    <item>
      <title>Managed vs interactive Cloudflare challenge: how to tell, and why it changes your fix</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 13:53:15 +0000</pubDate>
      <link>https://dev.to/bshahin/managed-vs-interactive-cloudflare-challenge-how-to-tell-and-why-it-changes-your-fix-3in0</link>
      <guid>https://dev.to/bshahin/managed-vs-interactive-cloudflare-challenge-how-to-tell-and-why-it-changes-your-fix-3in0</guid>
      <description>&lt;h1&gt;
  
  
  Managed vs interactive Cloudflare challenge: how to tell, and why it changes your fix
&lt;/h1&gt;

&lt;p&gt;You send a request, Cloudflare blocks it, and you reach for “a Turnstile solver.” But half the time that’s the wrong tool — because what stopped you wasn’t a Turnstile widget at all, it was a &lt;em&gt;managed challenge&lt;/em&gt;. They look similar from the outside and get lumped together as “Cloudflare captcha,” but they’re different mechanisms that need opposite handling. Picking the wrong one is why people burn hours (and solver credits) getting nowhere.&lt;/p&gt;

&lt;p&gt;Here's how to tell which one you hit, straight from the response, and what each actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things people conflate
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interactive Turnstile widget.&lt;/strong&gt; An embedded &lt;code&gt;cf-turnstile&lt;/code&gt; widget sitting on an otherwise-normal page — usually a login or signup form. The page loads fine; the widget produces a token (&lt;code&gt;cf-turnstile-response&lt;/code&gt;) that you submit with the form. The site verifies that token server-side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed challenge.&lt;/strong&gt; Cloudflare’s full-page interstitial — the “Checking your browser before you access…” screen — that gates the &lt;em&gt;entire&lt;/em&gt; page before any real content loads. It runs a JavaScript challenge, scores the browser, and on success issues a &lt;code&gt;cf_clearance&lt;/code&gt; cookie that lets subsequent requests through.&lt;/p&gt;

&lt;p&gt;The confusion is understandable: a managed challenge can itself &lt;em&gt;render&lt;/em&gt; a Turnstile widget as part of the interstitial. But the unit of work is completely different — a standalone widget gives you a &lt;strong&gt;token to submit&lt;/strong&gt;; a managed challenge gives you a &lt;strong&gt;clearance cookie to carry&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell them apart (from the response)
&lt;/h2&gt;

&lt;p&gt;Don't guess from the screenshot — read what comes back.&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;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;get&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;headers&lt;/span&gt;&lt;span class="o"&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;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="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;headers&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;cf-mitigated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# "challenge" =&amp;gt; managed challenge layer
&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;r&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;# a widget present in the DOM
&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;challenge-platform&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;r&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;# CF interstitial script =&amp;gt; managed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactive widget:&lt;/strong&gt; the page returns &lt;code&gt;200&lt;/code&gt; with real content, and the HTML contains a &lt;code&gt;cf-turnstile&lt;/code&gt; element with a &lt;code&gt;sitekey&lt;/code&gt;. The captcha is &lt;em&gt;part of a form&lt;/em&gt;, not the whole page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed challenge:&lt;/strong&gt; the response is the interstitial itself — typically &lt;code&gt;403&lt;/code&gt;/&lt;code&gt;503&lt;/code&gt;, a &lt;code&gt;cf-mitigated: challenge&lt;/code&gt; header, a body containing &lt;code&gt;cdn-cgi/challenge-platform&lt;/code&gt; scripts, and &lt;strong&gt;no real page content&lt;/strong&gt;. You never reach the actual HTML until you clear it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither (rule this out first):&lt;/strong&gt; a flat &lt;code&gt;403&lt;/code&gt; carrying a Cloudflare error code like &lt;code&gt;1020&lt;/code&gt; (firewall rule) or &lt;code&gt;1006/1007&lt;/code&gt; (IP ban) is a WAF/IP decision — no token or clearance fixes that; you need a different egress IP or to stop tripping the rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it changes your fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interactive widget → token flow.&lt;/strong&gt; Read the sitekey off the &lt;em&gt;rendered&lt;/em&gt; widget + the page URL, get a token, inject it into the response field, and submit immediately (tokens are single-use and short-lived). You do &lt;strong&gt;not&lt;/strong&gt; need a full browser session for the rest — just a valid token at submit time.&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&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;PAGEURL&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x4AAAAA...&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.example/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rid&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://ocr.captchaai.com/in.php&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;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;method&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;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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageurl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PAGEURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;request&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="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;res&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://ocr.captchaai.com/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="mi"&gt;1&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;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="c1"&gt;# submit token in cf-turnstile-response immediately
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Managed challenge → clearance flow.&lt;/strong&gt; A bare token is &lt;em&gt;not enough&lt;/em&gt;. You need to actually pass the interstitial — in a real browser or a solver that runs the JS challenge — to mint &lt;code&gt;cf_clearance&lt;/code&gt;, then reuse that cookie with the &lt;strong&gt;same IP + User-Agent + TLS fingerprint&lt;/strong&gt; that earned it. Change any of those and Cloudflare re-challenges. So the pattern is: mint clearance once, pin the session (one sticky IP, one UA, a browser-matching TLS fingerprint like &lt;code&gt;curl_cffi&lt;/code&gt;'s &lt;code&gt;impersonate&lt;/code&gt;), and re-mint when you see &lt;code&gt;cf-mitigated: challenge&lt;/code&gt; again rather than replaying a dead cookie.&lt;/p&gt;

&lt;p&gt;The expensive mistake is mixing them up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trying to “get a Turnstile token” for a &lt;strong&gt;managed challenge&lt;/strong&gt; — there’s often no standalone widget token to fetch; you needed clearance.&lt;/li&gt;
&lt;li&gt;Spinning up a full browser + clearance handling for a &lt;strong&gt;standalone widget&lt;/strong&gt; — overkill; a token would have done it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first move on any Cloudflare block is the 30-second classification above. It tells you whether you’re in token-land or clearance-land, and everything downstream depends on that.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the response first&lt;/strong&gt;: &lt;code&gt;cf-mitigated&lt;/code&gt; header + &lt;code&gt;challenge-platform&lt;/code&gt; in body + no real content → managed challenge. A &lt;code&gt;cf-turnstile&lt;/code&gt; sitekey on an otherwise-loaded page → interactive widget. &lt;code&gt;1006/1007/1020&lt;/code&gt; → IP/WAF, not solvable with a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive widget&lt;/strong&gt; → solve sitekey+pageurl, inject token, submit immediately (single-use, short TTL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed challenge&lt;/strong&gt; → mint &lt;code&gt;cf_clearance&lt;/code&gt; via a real browser/solver, pin IP+UA+TLS, re-mint on re-challenge.&lt;/li&gt;
&lt;li&gt;Classify before you reach for a tool — the wrong path is why “Cloudflare won’t solve” turns into a lost afternoon.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want to test either path against your own target, CaptchaAI handles both (Turnstile tokens and the full managed-challenge clearance flow) and is 2Captcha-API-compatible, so an existing client is mostly a base-URL change — &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cloudflare-managed-vs-interactive-challenge" rel="noopener noreferrer"&gt;the trial is free&lt;/a&gt; (3 days, no card).&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>cloudflare</category>
      <category>automation</category>
    </item>
    <item>
      <title>Cloudflare cf_clearance: why it expires and how to stop the re-challenge loop</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Tue, 16 Jun 2026 20:58:13 +0000</pubDate>
      <link>https://dev.to/bshahin/cloudflare-cfclearance-why-it-expires-and-how-to-stop-the-re-challenge-loop-do9</link>
      <guid>https://dev.to/bshahin/cloudflare-cfclearance-why-it-expires-and-how-to-stop-the-re-challenge-loop-do9</guid>
      <description>&lt;h1&gt;
  
  
  Cloudflare cf_clearance: why it expires and how to stop the re-challenge loop
&lt;/h1&gt;

&lt;p&gt;You solve the Cloudflare challenge, get a &lt;code&gt;cf_clearance&lt;/code&gt; cookie, make your next request — and Cloudflare throws you straight back to the challenge. You solve it again, same thing. The scraper is technically "working," it's just stuck in a loop, burning a solve on every request and never getting to the actual page.&lt;/p&gt;

&lt;p&gt;This is one of the most common Cloudflare problems, and it's almost never the challenge itself. It's how &lt;code&gt;cf_clearance&lt;/code&gt; is issued and what it's tied to. Once you understand that, the loop has a clean fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What cf_clearance actually is
&lt;/h2&gt;

&lt;p&gt;When you pass a Cloudflare challenge (managed challenge, JS challenge, or interactive Turnstile), Cloudflare issues a &lt;code&gt;cf_clearance&lt;/code&gt; cookie. That cookie is your proof of passage — present it on subsequent requests and Cloudflare lets you through without re-challenging.&lt;/p&gt;

&lt;p&gt;The catch is that &lt;code&gt;cf_clearance&lt;/code&gt; is &lt;strong&gt;not a portable token you can reuse anywhere&lt;/strong&gt;. It carries two constraints that cause the loop:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It has a TTL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cf_clearance&lt;/code&gt; expires. The window varies by site configuration (often tens of minutes, sometimes shorter under stricter settings or "Under Attack" mode). When it expires, the next request gets challenged again — expected behavior. The bug is when your code keeps replaying an expired cookie instead of noticing it's dead and minting a fresh one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It's bound to your IP + User-Agent + TLS fingerprint
&lt;/h3&gt;

&lt;p&gt;This is the one that traps people. Cloudflare binds the clearance to the &lt;strong&gt;exact context that solved the challenge&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;IP&lt;/strong&gt; that passed it,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;User-Agent&lt;/strong&gt; string presented, and&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;TLS/JA3 fingerprint&lt;/strong&gt; of the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change any of those between getting the cookie and using it, and Cloudflare treats the cookie as invalid and re-challenges. That's why these patterns loop forever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotating proxies mid-session&lt;/strong&gt; — you solve on IP A, your next request goes out IP B, cookie rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solving in a real browser, then submitting from &lt;code&gt;requests&lt;/code&gt;&lt;/strong&gt; — the browser's TLS fingerprint and UA don't match your HTTP client, so the cookie you carefully obtained is dead on the first reuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spoofing a UA that doesn't match the fingerprint&lt;/strong&gt; — claiming Chrome while sending a Python-&lt;code&gt;requests&lt;/code&gt; JA3 is itself a mismatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to confirm it's the loop and not something else
&lt;/h2&gt;

&lt;p&gt;Before fixing, log what you're actually getting back:&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;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;get&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;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookies&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;cf_clearance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clearance&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="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;headers&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;cf-mitigated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# "challenge" =&amp;gt; you got re-challenged
&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_clearance&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;r&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="c1"&gt;# did this response set a NEW one?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cf-mitigated: challenge&lt;/code&gt; (or a challenge-page body) → you're in the re-challenge loop; the cookie was rejected.&lt;/li&gt;
&lt;li&gt;A flat &lt;code&gt;403&lt;/code&gt; with a Cloudflare &lt;code&gt;1006/1007&lt;/code&gt; error code → that's an &lt;strong&gt;IP ban&lt;/strong&gt;, not a clearance problem; no cookie fixes it (you need a different egress IP).&lt;/li&gt;
&lt;li&gt;A clean &lt;code&gt;200&lt;/code&gt; → you're through; the cookie is valid for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The loop breaks when you stop reusing a stale/mismatched cookie and instead &lt;strong&gt;evict-and-re-mint on the challenge signal, with a pinned context&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin one IP + one UA + one TLS fingerprint per session.&lt;/strong&gt; Don't rotate the proxy mid-session. If you're submitting from Python, send a browser-matching TLS fingerprint (e.g. &lt;code&gt;curl_cffi&lt;/code&gt; with &lt;code&gt;impersonate&lt;/code&gt;) and the &lt;em&gt;same&lt;/em&gt; UA you solved with — not the default &lt;code&gt;requests&lt;/code&gt; fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect re-challenge by the response, not a timer.&lt;/strong&gt; Cloudflare can invalidate early, so don't trust a fixed "valid for N minutes" assumption. When you see &lt;code&gt;cf-mitigated: challenge&lt;/code&gt;, treat the stored clearance as dead, evict it, and re-mint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-mint with the same context that will reuse it.&lt;/strong&gt; Whatever solves the challenge (a real browser or a solver) must produce a cookie usable by the same IP+UA+fingerprint that makes the real requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally refresh proactively&lt;/strong&gt; just before the known TTL to avoid a user-facing stall.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal session loop that respects all of this:&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;curl_cffi.requests&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cc&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;cc&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;impersonate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chrome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# browser-matching TLS fingerprint
&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&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) ... Chrome/124.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&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;UA&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@residential-ip:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# one sticky IP for the session
&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;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;http&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="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="n"&gt;PROXY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&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;clearance&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;get&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;cookies&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;cf_clearance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clearance&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;clearance&lt;/span&gt; &lt;span class="k"&gt;else&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;r&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;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;cf-mitigated&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge&lt;/span&gt;&lt;span class="sh"&gt;"&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;challenge-platform&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;r&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;clearance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mint_clearance&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;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROXY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# re-mint with the SAME UA+IP
&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;get&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;cookies&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;cf_clearance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clearance&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;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clearance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point isn't the exact library — it's that the cookie, the UA, the IP, and the fingerprint all stay consistent, and a challenge response triggers a re-mint instead of another doomed retry.&lt;/p&gt;

&lt;p&gt;If you offload the challenge to a solving service, the same rule applies: it has to mint the clearance against the IP+UA you'll reuse, or hand back a token you submit immediately in a matched context. CaptchaAI handles the Cloudflare challenge flow this way and is 2Captcha-API-compatible, so an existing client is mostly a base-URL change — and if you want to test it against your own target, &lt;a href="https://captchaai.com/trial?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cf-clearance-expiry-rechallenge-loop" rel="noopener noreferrer"&gt;the trial is free&lt;/a&gt; (3 days, no card).&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] One IP + one UA + one TLS fingerprint, pinned for the whole session&lt;/li&gt;
&lt;li&gt;[ ] Don't rotate the proxy mid-session&lt;/li&gt;
&lt;li&gt;[ ] Submit with a browser-matching TLS fingerprint (not raw &lt;code&gt;requests&lt;/code&gt;) if you solved in a browser&lt;/li&gt;
&lt;li&gt;[ ] Detect re-challenge via &lt;code&gt;cf-mitigated&lt;/code&gt; / challenge body, evict + re-mint — don't replay a dead cookie&lt;/li&gt;
&lt;li&gt;[ ] Rule out a 1006/1007 IP ban first (different problem, needs a new IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get those right and the loop turns into a single solve followed by clean &lt;code&gt;200&lt;/code&gt;s — which is how it's supposed to behave.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>cloudflare</category>
      <category>automation</category>
    </item>
    <item>
      <title>Cloudflare Turnstile in Playwright: Why Your Tests Stall and How to Solve It in 8 Lines</title>
      <dc:creator>Bassem Shahin</dc:creator>
      <pubDate>Wed, 03 Jun 2026 21:38:36 +0000</pubDate>
      <link>https://dev.to/bshahin/cloudflare-turnstile-in-playwright-why-your-tests-stall-and-how-to-solve-it-in-8-lines-c22</link>
      <guid>https://dev.to/bshahin/cloudflare-turnstile-in-playwright-why-your-tests-stall-and-how-to-solve-it-in-8-lines-c22</guid>
      <description>&lt;h1&gt;
  
  
  Cloudflare Turnstile in Playwright: Why Your Tests Stall and How to Solve It in 8 Lines
&lt;/h1&gt;

&lt;p&gt;If you're running Playwright or Selenium against any site behind Cloudflare, you've already met Turnstile. It's the new "managed challenge" widget Cloudflare started shipping in 2023, and it now appears in front of login flows, contact forms, signup pages, and increasingly the entire site root.&lt;/p&gt;

&lt;p&gt;Here's the part most teams miss: &lt;strong&gt;Turnstile doesn't always show a checkbox.&lt;/strong&gt; A lot of the time it just sits invisible, runs its scoring loop, and either issues a token silently or stalls forever. Your test doesn't crash. It just times out at the next &lt;code&gt;page.click("button[type=submit]")&lt;/code&gt;. The CI log says "element not interactable." Nobody knows why.&lt;/p&gt;

&lt;p&gt;I work on CaptchaAI. I'm going to show you exactly what's happening, then drop in 8 lines that fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real scenario
&lt;/h2&gt;

&lt;p&gt;You have a Playwright suite that runs every PR. One day a test starts failing on the signup flow. You re-run it. It fails again. Locally on your laptop it passes. On CI it doesn't.&lt;/p&gt;

&lt;p&gt;What's actually happening: Cloudflare flagged your CI runner's IP block (GitHub Actions, GitLab runners, Hetzner, OVH, DO — all of them are on Cloudflare's "elevated risk" list). Turnstile decides to switch from invisible mode to "managed challenge" mode. Now there's a widget in the DOM that needs a real token before the form submit will accept.&lt;/p&gt;

&lt;p&gt;Your test never interacted with the widget because last week it didn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retries don't help
&lt;/h2&gt;

&lt;p&gt;The instinct is to add a &lt;code&gt;retry: 2&lt;/code&gt; and move on. Don't. Cloudflare's scoring is per-IP-per-fingerprint, and each retry from the same runner makes the next challenge harder, not easier. After ~3 attempts you'll get full block pages instead of the widget.&lt;/p&gt;

&lt;p&gt;The right move is to &lt;strong&gt;solve the widget once, inject the token, and submit normally&lt;/strong&gt; — exactly what a human user does, just faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Turnstile actually issues a token
&lt;/h2&gt;

&lt;p&gt;The widget renders an iframe pointing at &lt;code&gt;challenges.cloudflare.com&lt;/code&gt;. Inside the iframe it runs a fingerprint/behavior scoring loop for 1–4 seconds. When it's satisfied, it calls back into the parent page via a hidden &lt;code&gt;&amp;lt;input name="cf-turnstile-response"&amp;gt;&lt;/code&gt; and fills the value attribute with a JWT-shaped token. That token is what the backend validates against Cloudflare's &lt;code&gt;siteverify&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;To solve Turnstile programmatically you need three things from the page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;sitekey&lt;/strong&gt; — visible in the page source as &lt;code&gt;data-sitekey="0x4AAA..."&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;page URL&lt;/strong&gt; Turnstile was loaded on (Cloudflare validates this)&lt;/li&gt;
&lt;li&gt;A solver service that returns a valid token for that pair&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then you inject the token into the hidden input and trigger the form submit. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Here's a Playwright + Python integration. Endpoint and key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Submit: &lt;code&gt;https://ocr.captchaai.com/in.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Poll: &lt;code&gt;https://ocr.captchaai.com/res.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API key placeholder: &lt;code&gt;YOUR_API_KEY&lt;/code&gt; (get one at &lt;a href="https://captchaai.com/trial" rel="noopener noreferrer"&gt;https://captchaai.com/trial&lt;/a&gt; — 3 days, 5 threads, no card).
&lt;/li&gt;
&lt;/ul&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;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&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="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="n"&gt;TARGET_URL&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://example.com/signup&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;sitekey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. submit the task
&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://ocr.captchaai.com/in.php&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;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;method&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;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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageurl&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_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;20&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="k"&gt;if&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;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;status&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="mi"&gt;1&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;submit failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&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="n"&gt;task_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. poll for the token (typical solve: 8-20s)
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&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;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rr&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://ocr.captchaai.com/res.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&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;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;action&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;get&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rr&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;status&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="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="n"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# the cf-turnstile-response token
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rr&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;request&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAPCHA_NOT_READY&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;rr&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;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turnstile solve timed out&lt;/span&gt;&lt;span class="sh"&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;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="n"&gt;TARGET_URL&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;locator&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;first&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;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;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TARGET_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# inject the token where Cloudflare expects it
&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;(t) =&amp;gt; { document.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[name=cf-turnstile-response]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).value = t }&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="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_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/welcome&lt;/span&gt;&lt;span class="sh"&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;15000&lt;/span&gt;&lt;span class="p"&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;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same flow in Node.js with Playwright:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&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_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;TARGET_URL&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://example.com/signup&lt;/span&gt;&lt;span class="dl"&gt;'&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;solveTurnstile&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;pageUrl&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;submit&lt;/span&gt; &lt;span class="o"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ocr.captchaai.com/in.php&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;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;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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;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="nx"&gt;sitekey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageurl&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="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="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit failed: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&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="k"&gt;for &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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&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="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="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="mi"&gt;3000&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;poll&lt;/span&gt; &lt;span class="o"&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;`https://ocr.captchaai.com/res.php?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="s2"&gt;&amp;amp;action=get&amp;amp;id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;json=1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&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;poll&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&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;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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;));&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turnstile solve timed out&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&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;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TARGET_URL&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;sitekey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;locator&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-sitekey]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&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-sitekey&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;solveTurnstile&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;TARGET_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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;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;[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="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&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="p"&gt;);&lt;/span&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;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type=submit]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;waitForURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/welcome&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;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;Expected output: the form submits, you land on &lt;code&gt;/welcome&lt;/code&gt;, your test passes. Typical end-to-end overhead is 8–20 seconds for the solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;A few real gotchas I've watched teams hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong sitekey.&lt;/strong&gt; If the page lazy-loads Turnstile (some React/Next sites do this), &lt;code&gt;[data-sitekey]&lt;/code&gt; won't be in the DOM at &lt;code&gt;page.goto&lt;/code&gt;. Wait for it: &lt;code&gt;page.wait_for_selector('[data-sitekey]', timeout=10000)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Turnstile widgets.&lt;/strong&gt; Login + reset-password + signup pages can all render the widget. Use a more specific selector (parent form + &lt;code&gt;[data-sitekey]&lt;/code&gt;) so you don't solve the wrong one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token expired.&lt;/strong&gt; Turnstile tokens are valid for ~300 seconds. If your test does a long DB seed step between solving and submitting, re-solve right before the click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site uses Turnstile in &lt;code&gt;invisible&lt;/code&gt; mode.&lt;/strong&gt; Look for &lt;code&gt;data-size="invisible"&lt;/code&gt;. The flow is identical — you still inject the token into the hidden input. The only difference is there's no widget UI rendered at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend uses a non-default secret.&lt;/strong&gt; That's a Cloudflare configuration on the site owner's side; the token is still valid, your code is fine. If the backend rejects, the failure is at their &lt;code&gt;siteverify&lt;/code&gt; step, not yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this fits into a real CI pipeline
&lt;/h2&gt;

&lt;p&gt;What I do in my own setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run Playwright suites against a staging environment that's also behind Turnstile (don't whitelist your CI IPs at Cloudflare — that just hides the bug from yourself).&lt;/li&gt;
&lt;li&gt;Set the solver as a thin module: one &lt;code&gt;solve_turnstile&lt;/code&gt; function imported across every test that needs it.&lt;/li&gt;
&lt;li&gt;Keep concurrency aligned with your thread allocation. If your plan gives you 5 threads and you run 12 parallel Playwright workers, 7 of them will queue. The BASIC plan ($15/mo) gives 5 threads with unlimited solves — fine for one repo. For monorepos with many parallel suites, STANDARD (15 threads, $30/mo, or $27 on /lp/new-year-deals) is the realistic floor.&lt;/li&gt;
&lt;li&gt;Add a single retry on solver timeout — but not on Cloudflare block. The two failure modes need different handling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pricing for context (current as of 2026-06-02): unlimited solves on every plan, you pay only for thread concurrency. Trial is at &lt;a href="https://captchaai.com/trial" rel="noopener noreferrer"&gt;https://captchaai.com/trial&lt;/a&gt; (3 days, 5 threads, no card). If you want a discount, &lt;a href="https://captchaai.com/lp/new-year-deals" rel="noopener noreferrer"&gt;https://captchaai.com/lp/new-year-deals&lt;/a&gt; has up to 17% off on STANDARD through ENTERPRISE.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do next
&lt;/h2&gt;

&lt;p&gt;If you have a flaky test that times out at form submit on a Cloudflare-fronted page: drop the 8-line solver above into a helper, swap the sitekey lookup, and run it from a clean CI runner.&lt;/p&gt;

&lt;p&gt;— Bassem&lt;/p&gt;

&lt;p&gt;(disclosure: CaptchaAI maintainer)&lt;/p&gt;

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