Solving CAPTCHAs with Python
CAPTCHAs are the wall many scrapers hit last, and this guide — part of Bypassing Cloudflare and Akamai Protections — explains how third-party solver services work in Python, what they cost, and why avoiding the challenge entirely is almost always the better move.
Python does not "break" a CAPTCHA; it outsources the challenge to a solver service such as 2Captcha or Anti-Captcha. You send the service the page URL and the widget's site key, it returns a solution token (after human or automated solving), and you inject that token into the page or the form submission so the target server accepts the request. This works for reCAPTCHA, hCaptcha, and Cloudflare Turnstile, but it adds cost and latency per solve — so the best strategy is usually to structure your scrape so the CAPTCHA never appears.
How Solver Services Actually Work
A CAPTCHA widget is not solved on the page itself; solving it produces a token that the site's backend then verifies. The scraper's job is to obtain a valid token and present it exactly where the browser would. Solver services expose this as a simple two-step API: submit the challenge parameters, then poll for the result. For a checkbox reCAPTCHA v2 or an hCaptcha, the key parameters are the site key (a public identifier embedded in the page's HTML or widget script) and the page URL. The service solves the challenge on its side and returns a token string.
That token is the same value a real browser would place in a hidden form field (for reCAPTCHA, g-recaptcha-response) or hand to the site's JavaScript callback. You inject it and submit. Because the token is time-limited, you must use it quickly. Understanding where the token goes requires reading the target's challenge flow, which overlaps heavily with the detection analysis in Advanced Scraping Techniques and Anti-Bot Evasion.
The API Flow in Python
The pattern below submits an hCaptcha challenge to a 2Captcha-style endpoint, polls until the solution is ready, and returns the token. Every HTTP call sends an explicit User-Agent. Store your API key in an environment variable rather than hard-coding it. The polling loop backs off politely and gives up after a bounded time so a stuck solve does not hang forever — the same discipline described in Retrying Failed Requests with Tenacity.
import os
import time
import httpx
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36"}
API_KEY = os.environ["CAPTCHA_API_KEY"]
BASE = "https://2captcha.com"
def submit_challenge(client: httpx.Client, site_key: str, page_url: str) -> str:
resp = client.get(
f"{BASE}/in.php",
params={
"key": API_KEY,
"method": "hcaptcha",
"sitekey": site_key,
"pageurl": page_url,
"json": 1,
},
headers=HEADERS,
timeout=30.0,
)
resp.raise_for_status()
data = resp.json()
if data.get("status") != 1:
raise RuntimeError(f"submit failed: {data.get('request')}")
return data["request"] # the task id
def poll_solution(client: httpx.Client, task_id: str, timeout_s: float = 120.0) -> str:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
time.sleep(5)
resp = client.get(
f"{BASE}/res.php",
params={"key": API_KEY, "action": "get", "id": task_id, "json": 1},
headers=HEADERS,
timeout=30.0,
)
resp.raise_for_status()
data = resp.json()
if data.get("status") == 1:
return data["request"] # the solution token
if data.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(f"solve error: {data.get('request')}")
raise TimeoutError("captcha not solved within budget")
def solve_hcaptcha(site_key: str, page_url: str) -> str:
with httpx.Client() as client:
task_id = submit_challenge(client, site_key, page_url)
return poll_solution(client, task_id)
if __name__ == "__main__":
token = solve_hcaptcha("10000000-ffff-ffff-ffff-000000000001", "https://example.com/login")
print("token length:", len(token))
Injecting the Token and Submitting
Once you hold a token, you deliver it the way the site expects. For a classic form-based reCAPTCHA v2, the token goes into the g-recaptcha-response field of the POST body. For widgets wired to JavaScript callbacks — including many Turnstile and hCaptcha setups — you must run the page in a real browser and call the widget's callback with the token, which is why token injection often pairs with the browser automation in Using Playwright for Modern Web Automation.
import os
import httpx
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"}
def submit_login(page_url: str, username: str, password: str, captcha_token: str) -> httpx.Response:
payload = {
"username": username,
"password": password,
"g-recaptcha-response": captcha_token,
}
with httpx.Client() as client:
resp = client.post(page_url, data=payload, headers=HEADERS, timeout=30.0)
resp.raise_for_status()
return resp
if __name__ == "__main__":
token = os.environ.get("SOLVED_TOKEN", "demo-token")
result = submit_login("https://example.com/login", "user", "pass", token)
print("login status:", result.status_code)
For Cloudflare Turnstile specifically, the site key and page URL go to the solver just like hCaptcha, but Turnstile is tightly integrated with Cloudflare's broader challenge system — so a valid token is often necessary but not sufficient. You may also need the coherent IP posture covered in Rotating Proxies and Managing IP Blocks, because a solved token from a flagged connection still fails.
Avoiding CAPTCHAs in the First Place
Every solve costs money and time, so the highest-leverage tactic is not triggering the challenge at all. CAPTCHAs usually appear because something upstream flagged you: a datacenter IP, an inconsistent header set, a non-browser TLS fingerprint, or an aggressive request rate. Fix those and the challenge frequently never shows. Slow down, rotate clean residential-quality IPs, present coherent headers as in How to Rotate User-Agents in Python, and — where a static endpoint suffices — prefer the low-profile approach in How to Scrape a Static Website Without Getting Blocked. Solving CAPTCHAs should be the exception, not the architecture.
Edge Cases and Caveats
- Respect the law and the terms of service. Solving a CAPTCHA to access data you are not authorized to collect can breach a site's terms or relevant law. Confirm you have the right to scrape the target before automating around its protections.
- Tokens expire fast. A reCAPTCHA/hCaptcha token is typically valid for about two minutes. Submit it immediately; do not solve in a batch and reuse later.
- The site key is not the answer. The site key is a public identifier, not a secret. Solving still requires the service to complete the challenge for the specific page URL.
- A token can still be rejected. Backends score the token against IP, session, and behavior. A valid token from a flagged IP or an empty session often fails, so fix the underlying signals too.
- Budget for cost and latency. Solves cost a fraction of a cent to a few cents each and add seconds to minutes of latency. At scale this dominates both your bill and your throughput.
- Turnstile and enterprise reCAPTCHA are harder. Invisible and enterprise variants score behavior continuously, so a one-shot token may not clear them without a genuine browser context.
Frequently Asked Questions
Can Python solve a CAPTCHA on its own? Not reliably for modern reCAPTCHA, hCaptcha, or Turnstile. Python code coordinates the process, but the actual solving is delegated to a third-party service that uses human workers or specialized models. Your script submits the challenge, waits for a token, and injects it where the site expects it.
What do I send to a solver service? For widget-based CAPTCHAs you send the public site key (found in the page's HTML or widget script) and the page URL, plus the challenge type. The service returns a task id; you then poll a result endpoint until it hands back the solution token, which you place in the form field or callback the browser would use.
Why does my request still fail after I inject a valid token? Because the token is only one input the backend scores. It also weighs your IP reputation, session cookies, TLS fingerprint, and behavior. A token solved from a datacenter IP or a session with no history is frequently rejected, so you often need clean proxies and coherent headers alongside the token.
Is it better to solve CAPTCHAs or avoid them? Avoid them whenever possible. CAPTCHAs usually appear because an earlier signal flagged your traffic, so slowing down, rotating quality IPs, and sending browser-consistent headers often prevents the challenge entirely. Solving is a costly, higher-latency fallback for the cases where prevention is not enough.