Solve reCAPTCHA v2 in Playwright with Python
Step-by-step Playwright Python script to solve reCAPTCHA v2: find the sitekey, get a token, inject it, fire the callback, and fix the 4 common rejections.
To solve reCAPTCHA v2 in Playwright with Python, read the widget's sitekey from the page, get a token for that sitekey and URL from a solving API, write the token into the hidden g-recaptcha-response field, call the widget's callback if the page uses one, then submit. The callback is the step most tutorials leave out, and it is why the same code works on Google's demo page and silently fails on real sites. Below is a complete script, the reasoning behind each step, and the four failure modes that account for most "the token didn't work" reports. If you just want the short version, our Playwright integration page has it.
What you need before you start
You need Python 3.9 or newer, Playwright installed with a browser (pip install playwright requests then playwright install chromium), a solving API key, and permission to automate the site. For pages you own, skip all of this and use Google's test keys instead — more on that at the end.
Three facts from Google's own documentation shape the whole approach:
- The widget is a
div.g-recaptchawith adata-sitekeyattribute, and optionally adata-callbacknaming a function that receives the token (reCAPTCHA display docs). - On a form submit, the token travels as the
g-recaptcha-responsePOST parameter. - Each token is valid for two minutes and can be verified only once; an old or reused token fails with
timeout-or-duplicate(verification docs).
Step 1: Find the sitekey and the callback
Read both from the widget element before you ask for a token. The sitekey identifies the widget; the callback tells you how the page expects to be notified.
widget = page.locator("[data-sitekey]").first
sitekey = widget.get_attribute("data-sitekey")
callback = widget.get_attribute("data-callback") # None if the page doesn't use one
Some sites render the widget with JavaScript (grecaptcha.render) instead of the div. In that case the sitekey is usually in the iframe URL as the k= parameter of the google.com/recaptcha/api2/anchor frame, which you can read with page.frames.
Step 2: Get a token from the solving API
Send the type, sitekey and page URL, and block until the token is ready. With CaptchaNo that is one request to POST /v1/solve?wait=60; the response carries the token, the user agent it was produced with, and the cost of the solve.
import requests
CAPTCHANO_KEY = "cn-YOUR_API_KEY"
PROXY = "http://user:pass@host:port" # the same proxy the browser uses
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
def solve_recaptcha_v2(sitekey: str, url: str) -> str:
r = requests.post(
"https://api.captchano.com/v1/solve",
params={"wait": 60},
json={
"type": "recaptcha_v2",
"sitekey": sitekey,
"url": url,
"proxy": PROXY,
"userAgent": USER_AGENT,
},
headers={"Authorization": f"Bearer {CAPTCHANO_KEY}"},
timeout=75,
)
data = r.json()
if data.get("status") != "ready":
raise RuntimeError(data.get("error") or data)
return data["solution"]["token"]
Passing the proxy and user agent is optional for reCAPTCHA v2 but worth doing: the site's verification sees where the token was produced, and matching the browser removes one source of rejections. The full request shape and error codes are in the API reference.
Step 3: Inject the token and fire the callback
Write the token into every g-recaptcha-response field on the page, then call the named callback with it. Pages that submit a plain form only need the field; pages that enable their submit button or send an AJAX request from JavaScript only react to the callback.
page.evaluate(
"""([token, callback]) => {
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => {
el.value = token;
el.innerHTML = token;
});
if (callback && typeof window[callback] === 'function') {
window[callback](token);
}
}""",
[token, callback],
)
Because data-callback is a documented attribute, this does not depend on any of reCAPTCHA's internal objects. If a page registers its callback through grecaptcha.render instead, look for the function name in the page's own script that calls render.
Step 4: Put it together
Solve as late as possible, right before you submit, so the two-minute window is not spent on page navigation.
from playwright.sync_api import sync_playwright
TARGET = "https://www.google.com/recaptcha/api2/demo" # Google's public demo page
with sync_playwright() as p:
browser = p.chromium.launch(proxy={"server": "http://host:port", "username": "user", "password": "pass"})
context = browser.new_context(user_agent=USER_AGENT)
page = context.new_page()
page.goto(TARGET)
widget = page.locator("[data-sitekey]").first
sitekey = widget.get_attribute("data-sitekey")
callback = widget.get_attribute("data-callback")
token = solve_recaptcha_v2(sitekey, page.url)
page.evaluate(
"""([token, callback]) => {
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => { el.value = token; el.innerHTML = token; });
if (callback && typeof window[callback] === 'function') window[callback](token);
}""",
[token, callback],
)
page.click("#recaptcha-demo-submit")
page.wait_for_load_state("networkidle")
print(page.inner_text("body")[:200]) # the demo prints "Verification Success" on a valid token
browser.close()
Check the result the page actually shows, not just that the script ran. A tutorial that logs "solved" after clicking submit has not verified anything.
Why the token still gets rejected
Almost every rejection traces back to one of four causes:
| Symptom | Cause | Fix |
|---|---|---|
| Field is filled, page does nothing | Page waits for its callback | Call the data-callback function with the token |
| Server says the captcha is invalid after a delay | Token older than two minutes | Request the token right before submit; re-solve on retry |
| Works locally, fails from servers | Token and browser on different IPs or user agents | Pass the browser's proxy and user agent to the solve |
| Fails on the second submit | Token reused | Tokens are single-use; solve again for each submission |
We go deeper on the session side in why captcha tokens get rejected. Invisible reCAPTCHA v2 uses the same flow with "invisible": true in the request; score-based v3 is different and needs the page's action name — see the reCAPTCHA v3 solver page.
Audio-transcription libraries versus a solving API
Open-source libraries such as Playwright-reCAPTCHA solve v2 by switching to the audio challenge and transcribing it, which needs FFmpeg and a speech-recognition service. They are a reasonable choice for low volumes on your own machine. They depend on the audio challenge being offered, which reCAPTCHA can withhold from sessions it already distrusts, and they keep a browser busy for the length of the challenge. A token API keeps your browser free and gives you the same call for Turnstile and other challenge types when the site changes provider.
Testing your own forms: use Google's test keys
If the form is yours, do not solve your own CAPTCHA in CI. Google's reCAPTCHA FAQ publishes a v2 test site key and secret that always pass verification and display a "for testing only" notice. Point your staging build at them and your Playwright suite tests your form logic without any external dependency. Our QA testing guide covers when a real solve is still worth it, such as end-to-end runs against a partner's environment.
Frequently Asked Questions
Can Playwright solve reCAPTCHA by itself?
Not reliably. Playwright controls the browser but has no way to answer an image challenge, and automated browsers are more likely to be shown one. You either use an audio-transcription library, which depends on the audio option being offered, or request a token from a solving API and inject it into the page as shown in this guide.
Where is the reCAPTCHA sitekey on a page?
Usually in the data-sitekey attribute of the element with the g-recaptcha class. When the widget is rendered from JavaScript instead, the key appears as the k parameter in the URL of the reCAPTCHA anchor iframe. Both are public values, since the browser needs them to load the widget.
How long is a reCAPTCHA v2 token valid?
Two minutes, according to Google's verification documentation, and it can be verified only once. After that the site's server receives a timeout-or-duplicate error. Request the token immediately before submitting the form, and request a fresh one on every retry rather than reusing the first.
Why does injecting the token not submit the form?
Many sites do not read the hidden field until their own JavaScript runs. The widget normally calls a function named in its data-callback attribute, and that function enables the button or sends the request. Setting the field alone skips that step, so read the callback name and call it with the token.
Is it legal to solve reCAPTCHA with Playwright?
The technique itself is common in testing and data collection, but whether you may use it depends on the site's terms and the law where you operate. Use it on your own properties, systems you have written permission to test, and data you are allowed to collect. For your own forms, Google's test keys are simpler.
Run it against your own target
Start a free CaptchaNo trial, create a key in the dashboard and swap it into the script above. The reCAPTCHA v2 solver page lists the options for invisible and enterprise widgets.