How to solve a PopularCaptcha in Python (working example, 2026)
If your Python script has hit a PopularCaptcha, you do not need a browser farm or a model of your own. You need the page's sitekey, one POST request, and the token that comes back.
This guide takes you from zero to a working token in about five minutes: get an API key, find the sitekey, request the token, submit it to the form, and handle the failure cases that show up once a script runs unattended. Failed solves are free, so a retry costs you only time.
Scarnox solves PopularCaptcha today, proxyless or with your own proxy. Other captcha types such as reCAPTCHA, Cloudflare Turnstile and AWS WAF are coming soon, so every example here uses PopularCaptcha. The captcha types overview explains how the others differ.
1. Get an API key
Create an account and start with free trial credit, no card needed. Your API key is on the dashboard. Keep it out of your source code and read it from an environment variable instead:
export SCARNOX_KEY="scarnox_YOURAPIKEY"
2. Find the sitekey
Open the page with the captcha, open DevTools, and search the Elements panel for data-sitekey. PopularCaptcha sitekeys are UUIDs:
<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>
No data-sitekey in the HTML? The widget is probably rendered by JavaScript. Check the Network tab for the widget's configuration request; its sitekey query parameter is the one you need. Other ways to find it cover the iframe URL and invisible widgets.
Send the URL of the page that shows the widget as site_url. The host matters most, but the full page URL is the safe choice.
3. Request a token
import os
import requests
API_URL = "https://api.scarnox.com/api/tasks/create"
API_KEY = os.environ["SCARNOX_KEY"]
def solve_captcha(sitekey: str, page_url: str) -> str:
r = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=185, # the call blocks until the token is ready
)
data = r.json()
if data.get("errorId") != 0:
raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
return data["solution"]["token"]
if __name__ == "__main__":
token = solve_captcha("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup")
print(token[:40], "...")
That is the whole integration. There is no polling loop and no second "get result" call. The request returns when the token is ready, so the one setting that matters is a timeout long enough to cover a slow solve. Use about 185 seconds.
A successful response has errorId set to 0, with the token under solution.token. Any other errorId means the solve failed, and the response carries an errorCode and an errorDescription that say why.
4. Submit the token
The token goes wherever the site expects the captcha response. For PopularCaptcha that is almost always a form field called h-captcha-response. Many sites also accept g-recaptcha-response, so send both:
import requests
def sign_up(session: requests.Session, form_url: str, email: str, sitekey: str) -> requests.Response:
token = solve_captcha(sitekey, form_url)
return session.post(form_url, data={
"email": email,
"h-captcha-response": token,
"g-recaptcha-response": token,
}, timeout=30)
Two details matter here. Real sites usually also check the session cookies from the page load, so load the form with the same requests.Session before you post. And post to the form's action URL, which is not always the page URL.
Solve right before you submit. The token expires within about two minutes, so do not fetch it at the start of a long job and hold on to it. If the site rejects the token, open the form in DevTools and confirm the hidden field names.
5. Handle failures
Failed solves are free, so a retry costs time and nothing else. Some errors will never succeed on a retry, though, so do not loop blindly. Check errorId every time, log errorCode and errorDescription, and retry only the transient failures.
Network errors need their own handling, because requests raises before any response exists:
import requests
try:
token = solve_captcha(sitekey, page_url)
except requests.RequestException as exc:
print("network problem, retry later:", exc)
except RuntimeError as exc:
print("solve rejected:", exc)
The error handling guide has a complete retry policy with backoff and jitter.
Common mistakes
- **Wrong
site_url.** Send the page that displays the widget, not the homepage. - Stale token. A token used after about two minutes is rejected. Solve right before you submit.
- Missing session. A post without the page's cookies can fail even with a valid token.
- **Some sites need
rqdata.** If the page supplies anrqdatavalue for the widget, pass it in the request as the optionalrqdatafield. - Invisible widget. If there is no checkbox and the site starts the widget from its own code, add
"is_invisible": trueto the request. The invisible captcha guide shows how to hand the token back.
Try it free
Create an account and start with free trial credit, no card needed. Run the example above against a form you own before you wire it into a larger job. For what solving costs at volume, see current pricing.
Logging each solve
Log one line per solve: the sitekey, the page host, the errorId, the errorCode on failure, and how long the call took. Leave out the API key and any proxy password. Those lines tell you whether a slow run is slow because of the page, the proxy or the solver. They are also the first thing you will need if you open a support request.
Solving in a loop
If your script solves many captchas in a row, keep the loop simple. Solve, submit, check the result, and only then move on. Do not solve ahead of time and queue tokens, because each one expires within about two minutes. A short pause between solves on the same site keeps your request rate close to what a human would produce.