How to Solve CAPTCHA in SeleniumBase Tests (Python)

SeleniumBase drives the browser. It never reads the CAPTCHA for you. So a SeleniumBase captcha step is three moves: pull the sitekey out of the page, ask a solver for a token, then write that token back into the form and submit. The solve happens outside the browser, which is why the same code works in a headed run, a headless run and a CI job. Here it is in both of the syntax formats you are most likely to be using.
What you need
- Python 3.10 or newer, with SeleniumBase and the CapSkip SDK installed.
- A page that actually renders a widget. Google’s own reCAPTCHA test keys always pass, so they prove nothing about your code.
- CapSkip running, either in Local mode on the loopback address or in Server mode on a machine your test runner can reach. Both are described under connection settings, and which one you want depends on where your tests execute.
# Both packages, one command. pip install seleniumbase capskip
What UC mode does, and what it does not do
SeleniumBase ships UC mode, and it is worth being precise about its job because people reach for it expecting a solver. UC mode reduces the automation signals a browser leaks, and its GUI helpers can click a checkbox on a Cloudflare interstitial. That is a different problem from producing a reCAPTCHA token. Nothing in UC mode reads a distorted image or picks the tiles with a bus in them.
# pip install seleniumbase
from seleniumbase import SB
with SB(uc=True) as sb:
# Disconnects the driver briefly so the page loads unobserved.
sb.uc_open_with_reconnect("https://example.com/protected", reconnect_time=3)
# Drives the real mouse pointer, so it needs a desktop session
# or a virtual display. It clicks a checkbox; it solves nothing.
sb.uc_gui_click_captcha()Two consequences follow. The GUI helpers need a real display, which rules them out of most headless CI. And for reCAPTCHA and GeeTest you still need a token, which is what the rest of this post covers. Use both together: UC mode to lower how often you get challenged, and a solver for the challenges that arrive anyway.
Step 1: read the sitekey off the page
The sitekey is public. It sits on the widget element as an attribute, and SeleniumBase can read it with one call, so you never have to hardcode it per environment.
# pip install seleniumbase
from seleniumbase import SB
PAGE = "https://example.com/page-with-recaptcha"
with SB() as sb:
sb.open(PAGE)
# Present on the widget div for v2, and on the script tag for v3.
sitekey = sb.get_attribute("[data-sitekey]", "data-sitekey")
print(sitekey) # this plus the page URL is all the solver needsStep 2: get a token from the solver
This part has nothing to do with SeleniumBase. It is an ordinary Python call to a service on your own machine, and it returns the token string the page would have produced if a person had passed the widget.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# v2 checkbox is the default shape. Pass invisible=1 or
# enterprise=1 for those variants, or version="v3" for v3.
result = solver.recaptcha(
sitekey=sitekey,
url=PAGE,
)
token = result["code"] # the value the form is waiting forThe same client handles the other types you are likely to meet in a test suite. Turnstile is solver.turnstile with the sitekey and page URL, GeeTest is solver.geetest with the gt and challenge values, and an image CAPTCHA is solver.normal with a file path, a URL or a base64 data URI. The Python CAPTCHA solver page covers the full surface.
Step 3: inject the token and submit
Google puts a hidden textarea in the form and expects the token in its value. Write to it with a script call, then submit the form the way the page’s own UI would.
# The response field is hidden, so a normal type() will not reach it.
sb.execute_script(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
token,
)
sb.click("button[type='submit']")
sb.assert_element(".signup-success")If the page wires its own callback function to the widget rather than reading the field on submit, call that function with the token instead of clicking. Which one you need is visible in the widget markup: a data-callback attribute names the function, and its absence means the form reads the field.
The full test
Here is the whole thing as a BaseCase class, which is the format that fits an existing pytest suite with no extra wiring.
# pip install seleniumbase capskip
from seleniumbase import BaseCase
from capskip import CapSkip
BaseCase.main(__name__, __file__)
PAGE = "https://example.com/page-with-recaptcha"
class RecaptchaTest(BaseCase):
def test_signup_form(self):
self.open(PAGE)
sitekey = self.get_attribute("[data-sitekey]", "data-sitekey")
solver = CapSkip(host="127.0.0.1", port=8080)
token = solver.recaptcha(sitekey=sitekey, url=PAGE)["code"]
self.execute_script(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
token,
)
self.click("button[type='submit']")
self.assert_element(".signup-success")Solve late. A reCAPTCHA token is good for about two minutes, so fetching one at the top of a test and using it after four other steps is a race you will lose intermittently. Put the solve immediately before the submit, which is also where a retry belongs.
Running the solver on a server
Test runners are rarely the machine you sit at. CapSkip covers that with a second connection mode, and the only thing that changes in your code is the host.
| Mode | Listens on | Use it when |
|---|---|---|
| Local | 127.0.0.1, that device only | Tests and solver run on one machine |
| Server | Your network address or public IP | A CI runner, a VPS or a test grid calls in |
# pip install capskip
import os
from capskip import CapSkip
# The SDK reads these names from the environment too, so one test
# file runs unchanged on a laptop and on a shared runner.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)A static public IP is recommended when the callers sit outside your own network. Server mode is still your hardware and still unmetered: it changes where the solver runs, never who owns it or what it costs per solve. The full set of options lives under connection settings.
Common errors
| What you see | Cause | Fix |
|---|---|---|
| NetworkException | Nothing is listening on that host and port | Start CapSkip, or point the host at the server running it |
| TimeoutException | Polling ran past recaptchaTimeout | Raise the timeout, or check the sitekey is the live one |
| ERROR_GOOGLEKEY | The sitekey never arrived or is malformed | Print what get_attribute returned before you send it |
| ERROR_PAGEURL | The page URL is missing or is not a full URL | Send the same absolute URL the browser is on |
| Form rejects a valid token | The page uses a callback rather than the field | Call the data-callback function with the token |
Every code the API can return is listed in the CapSkip API documentation, with what triggers each one.
FAQ
Does UC mode solve reCAPTCHA by itself?
No. UC mode makes the browser look less automated and can click a checkbox on a Cloudflare interstitial through the GUI helpers. Neither of those produces a reCAPTCHA token. You still fetch the token from a solver and write it into the form, exactly as shown above. Run both: UC mode lowers how often you are challenged, the solver clears the challenges you get.
Can I run this headless in CI?
Yes, because the solve never touches the browser. Pass headless to SB or add the flag on the pytest command line and the three steps are unchanged. The part that does not survive a headless runner is the UC mode GUI clicking, which drives a real mouse pointer and needs a desktop session or a virtual display.
My tests run on a Linux runner. Where does the solver go?
On a Windows machine you control, reached in Server mode. CapSkip is a Windows application, so the pattern is one solver instance on a Windows box and any number of runners pointing at it over the API. Set CAPSKIP_HOST on the runner and nothing else in the test changes.
Do I need a proxy for the solve to match my browser?
Sometimes, and it is supported for reCAPTCHA, Turnstile and GeeTest. Pass a proxy dictionary with a type and a URI and the solve is made through it, which matters when the site cares that the token and the session come from the same address. Image CAPTCHAs take no proxy, because nothing about them is bound to a network path.
The short version
Read the sitekey with get_attribute, fetch the token, write it into the hidden response field, submit. Keep the solve in the same step as the submit so the token is fresh, and keep the host in an environment variable so the suite runs the same everywhere. For the wider Selenium picture see the Selenium CAPTCHA solver page, and for v2 specifics see the reCAPTCHA v2 solver page. That leaves cost, which is where a suite that runs on every commit hurts most: local captcha solver means every solve happens on hardware you already own, unmetered, however often the tests fire.
