reCAPTCHA v2 is still one of the most common checkpoint challenges in login, signup, checkout, and form-submission flows. If your automation workflow hits a reCAPTCHA v2 checkbox or image challenge, you can solve it through an API in four steps: extract the sitekey and pageurl from the page, send them to the CaptchaAI reCAPTCHA v2 solver, poll for the result, and inject the returned token into the protected flow.
This guide walks through every step with working Python and Node.js code. It is written for developers who need a working integration, not a theory overview.
Not sure which reCAPTCHA version you're dealing with? Read How to Identify reCAPTCHA Version first.
What you need before you start
| Requirement | Details |
|---|---|
| CaptchaAI API key | Get yours from captchaai.com/api.php. It is a 32-character string. |
| Target page URL | The full URL where the reCAPTCHA v2 widget loads. |
| reCAPTCHA v2 sitekey | The public key tied to the widget instance on the page. |
| Runtime environment | Python 3.7+ with requests, or Node.js 18+ with built-in fetch. |
| Token submission path | A browser automation tool (Selenium, Puppeteer, Playwright) or an HTTP request flow that can submit the solved token. |
How to extract the sitekey and pageurl
The two required inputs are the sitekey and the pageurl. Getting either one wrong is the most common reason a solve request fails.
Finding the sitekey
The sitekey is a public key Google assigns to the reCAPTCHA widget on a given page. You can find it in three places:
Option 1 — the data-sitekey attribute on the widget container:
<div class="g-recaptcha" data-sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"></div>
Option 2 — the anchor URL loaded inside the reCAPTCHA iframe:
https://www.google.com/recaptcha/api2/anchor?k=6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-&...
The value of the k parameter is the sitekey.
Option 3 — a grecaptcha.render() call in the page's JavaScript:
grecaptcha.render('captcha-container', {
sitekey: '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
callback: onSuccess
});
Setting the pageurl
Use the full URL of the page where the widget loads — including the protocol. For example:
https://example.com/login
If the reCAPTCHA widget loads inside an iframe hosted on a different subdomain, use the iframe's source URL as the pageurl, not the parent page.
Important: If the
sitekeyandpageurlpair is invalid, the CaptchaAI API returnsERROR_BAD_TOKEN_OR_PAGEURL. Double-check both values before debugging anything else.
How the solving flow works
Page → extract sitekey + pageurl
↓
POST to in.php (method=userrecaptcha)
↓
receive captcha ID
↓
wait 15–20 seconds
↓
GET res.php (action=get, id=…)
↓ ↓
CAPCHA_NOT_READY status=1 → token
(wait 5s, retry) ↓
inject into g-recaptcha-response
↓
submit form / callback
- Submit —
POSTtohttps://ocr.captchaai.com/in.phpwithmethod=userrecaptcha, your API key, thegooglekey(sitekey), and thepageurl. The API returns a captcha ID. - Wait — Pause 15–20 seconds. reCAPTCHA v2 solves typically complete in under 60 seconds with a 99.5%+ success rate.
- Poll —
GEThttps://ocr.captchaai.com/res.phpwithaction=getand the captcha ID. If the result isCAPCHA_NOT_READY, wait 5 seconds and retry. - Receive — When solved, the API returns the reCAPTCHA response token (a long encoded string).
- Inject — Place the token into the
g-recaptcha-responsetextarea on the page, or pass it to the page's callback function. - Submit — Trigger the form submission or the action the target page expects after a successful reCAPTCHA solve.
Python implementation
import time
import requests
API_KEY = "YOUR_CAPTCHAAI_API_KEY"
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
PAGE_URL = "https://example.com/login"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
def solve_recaptcha_v2(api_key, sitekey, pageurl):
"""Submit a reCAPTCHA v2 challenge and return the solved token."""
# Step 1: Submit the captcha
submit_resp = requests.post(
SUBMIT_URL,
data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
},
timeout=30,
)
submit_resp.raise_for_status()
submit_data = submit_resp.json()
if submit_data.get("status") != 1:
raise RuntimeError(f"Submit failed: {submit_data}")
captcha_id = submit_data["request"]
print(f"Task created — captcha ID: {captcha_id}")
# Step 2: Wait before first poll
time.sleep(15)
# Step 3: Poll for result
for _ in range(60): # max ~5 minutes of polling
result_resp = requests.get(
RESULT_URL,
params={
"key": api_key,
"action": "get",
"id": captcha_id,
"json": 1,
},
timeout=30,
)
result_resp.raise_for_status()
result_data = result_resp.json()
if result_data.get("request") == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result_data.get("status") == 1:
return result_data["request"]
raise RuntimeError(f"Polling error: {result_data}")
raise TimeoutError("reCAPTCHA v2 solve timed out")
# Usage
token = solve_recaptcha_v2(API_KEY, SITEKEY, PAGE_URL)
print(f"Solved token: {token[:80]}...")
What this does:
- Sends the sitekey and pageurl to
in.phpwithmethod=userrecaptcha. - Waits 15 seconds, then polls
res.phpevery 5 seconds. - Returns the solved token as a string, ready for injection.
- Includes a safety limit of 60 poll attempts to prevent infinite loops.
Node.js implementation
const API_KEY = "YOUR_CAPTCHAAI_API_KEY";
const SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-";
const PAGE_URL = "https://example.com/login";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveRecaptchaV2(apiKey, sitekey, pageurl) {
// Step 1: Submit the captcha
const submitResp = await fetch(SUBMIT_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
key: apiKey,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: "1",
}),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) {
throw new Error(`Submit failed: ${JSON.stringify(submitData)}`);
}
const captchaId = submitData.request;
console.log(`Task created — captcha ID: ${captchaId}`);
// Step 2: Wait before first poll
await sleep(15_000);
// Step 3: Poll for result
for (let i = 0; i < 60; i++) {
const resultResp = await fetch(
`${RESULT_URL}?${new URLSearchParams({
key: apiKey,
action: "get",
id: captchaId,
json: "1",
})}`
);
const resultData = await resultResp.json();
if (resultData.request === "CAPCHA_NOT_READY") {
await sleep(5_000);
continue;
}
if (resultData.status === 1) {
return resultData.request;
}
throw new Error(`Polling error: ${JSON.stringify(resultData)}`);
}
throw new Error("reCAPTCHA v2 solve timed out");
}
// Usage
solveRecaptchaV2(API_KEY, SITEKEY, PAGE_URL)
.then((token) => console.log(`Solved token: ${token.slice(0, 80)}...`))
.catch(console.error);
PHP implementation (optional)
<?php
$apiKey = "YOUR_CAPTCHAAI_API_KEY";
$sitekey = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-";
$pageurl = "https://example.com/login";
// Step 1: Submit
$submit = file_get_contents("https://ocr.captchaai.com/in.php?" . http_build_query([
"key" => $apiKey,
"method" => "userrecaptcha",
"googlekey" => $sitekey,
"pageurl" => $pageurl,
"json" => 1,
]));
$submitData = json_decode($submit, true);
if ($submitData["status"] !== 1) {
die("Submit failed: " . $submit);
}
$captchaId = $submitData["request"];
echo "Task created — captcha ID: $captchaId\n";
// Step 2: Wait and poll
sleep(15);
for ($i = 0; $i < 60; $i++) {
$result = file_get_contents("https://ocr.captchaai.com/res.php?" . http_build_query([
"key" => $apiKey,
"action" => "get",
"id" => $captchaId,
"json" => 1,
]));
$resultData = json_decode($result, true);
if ($resultData["request"] === "CAPCHA_NOT_READY") {
sleep(5);
continue;
}
if ($resultData["status"] === 1) {
echo "Solved token: " . substr($resultData["request"], 0, 80) . "...\n";
exit(0);
}
die("Polling error: " . $result);
}
die("Solve timed out");
How to inject the solved token
Getting a token is not the final step. The token must reach the target page in the way it expects.
Method 1: Direct field injection (most common)
Set the value of the hidden g-recaptcha-response textarea. In Selenium (Python):
driver.execute_script(
'document.getElementById("g-recaptcha-response").innerHTML = arguments[0];',
token
)
driver.find_element("css selector", "form#login-form").submit()
In Puppeteer (Node.js):
await page.evaluate((token) => {
document.getElementById("g-recaptcha-response").innerHTML = token;
}, token);
await page.click("#submit-button");
Method 2: Callback function
Some pages define a data-callback on the widget or pass a callback to grecaptcha.render(). In that case, call the callback with the token:
// Find the callback name in the widget's data-callback attribute
// or in grecaptcha.render({ callback: myCallback })
await page.evaluate((token) => {
// Direct call if you know the function name
myCallbackFunction(token);
// Or via the internal config
// ___grecaptcha_cfg.clients[0].aa.l.callback(token);
}, token);
Method 3: Request-based injection
If you are not using a browser, include the token in the POST body that normally follows the reCAPTCHA solve:
response = requests.post("https://example.com/login", data={
"username": "user@example.com",
"password": "secure_password",
"g-recaptcha-response": token,
})
Common mistakes
| # | Mistake | What happens | Fix |
|---|---|---|---|
| 1 | Wrong sitekey | API returns ERROR_BAD_TOKEN_OR_PAGEURL or token is rejected by the page |
Re-extract the sitekey from data-sitekey or the anchor URL k parameter |
| 2 | Wrong pageurl | Same as above — the sitekey/pageurl pair does not match | Use the exact URL where the widget loads, including protocol |
| 3 | Mixing reCAPTCHA versions | Submitting a v3 or enterprise challenge as v2 | Check the widget type first — see How to Identify reCAPTCHA Version |
| 4 | Polling too aggressively | Unnecessary API load, possible rate limiting | Wait 15–20 seconds before the first poll, then 5 seconds between attempts |
| 5 | Token injected into wrong field | Form submits but the page rejects the response | Verify whether the page uses g-recaptcha-response, a callback, or a custom field |
| 6 | Invisible reCAPTCHA sent without invisible=1 |
Solve may fail silently | Add invisible=1 to the request if the widget is invisible — see Common reCAPTCHA Invisible Errors |
Troubleshooting
ERROR_WRONG_USER_KEY or ERROR_KEY_DOES_NOT_EXIST
Your API key is malformed or inactive. Verify the 32-character key from captchaai.com/api.php.
ERROR_ZERO_BALANCE
No free threads available. Either wait for threads to free up or upgrade your plan.
ERROR_PAGEURL
The pageurl parameter is missing. Add the full page URL to your request.
ERROR_BAD_TOKEN_OR_PAGEURL
The sitekey/pageurl pair is invalid. The most common cause: the reCAPTCHA widget loads inside an iframe on a different subdomain — use that iframe's URL, not the parent page URL.
CAPCHA_NOT_READY
Normal. The solve is still in progress. Wait 5 seconds and poll again.
ERROR_CAPTCHA_UNSOLVABLE
The challenge could not be solved. Retry with a fresh request.
Token returned but page still rejects it
This is almost always a token injection problem, not an API problem:
- Wrong field — Confirm the page uses
g-recaptcha-responseand not a callback. - Wrong page context — If the widget is in an iframe, the target form may be in a different frame.
- Token expired — reCAPTCHA tokens expire after about 2 minutes. Use the token immediately.
- Post-token flow wrong — The form submission, AJAX call, or callback execution after injection may be incorrect. Inspect network traffic from a manual solve to compare.
For the complete error reference, see Common reCAPTCHA v2 Solving Errors.
Why CaptchaAI works for this
| Factor | Detail |
|---|---|
| Success rate | 99.5%+ for reCAPTCHA v2 |
| Solve speed | Under 60 seconds |
| Integration | Standard HTTP API — works with any language or framework |
| Pricing | Thread-based plans starting at $15/month for unlimited solves |
| Same pattern | The submit → poll → result flow is identical for reCAPTCHA v3, Turnstile, GeeTest, and other types |
The biggest practical advantage: the exact same code pattern (submit to in.php, poll res.php) works across every captcha type CaptchaAI supports. Learn it once, and you can extend it to Cloudflare Turnstile, GeeTest v3, and more.
FAQ
What is the sitekey in reCAPTCHA v2?
The sitekey is the public key Google assigns to a specific reCAPTCHA widget instance. You need it together with the page URL to create a solve request. Find it in the data-sitekey attribute, the anchor iframe URL (k parameter), or in a grecaptcha.render() call.
How long does reCAPTCHA v2 solving take?
CaptchaAI solves reCAPTCHA v2 in under 60 seconds with a 99.5%+ success rate. Wait 15–20 seconds before your first poll, then check every 5 seconds.
Can I use the solved token in Selenium or Puppeteer?
Yes. Inject the token into the g-recaptcha-response field using JavaScript execution, then submit the form. If the page uses a callback, call that callback with the token instead.
How do I know the site uses reCAPTCHA v2 and not v3 or Enterprise?
Look for the "I'm not a robot" checkbox, a data-sitekey attribute on a widget container, or the absence of the render=sitekey pattern in api.js loading (which indicates v3). For the full detection method, read How to Identify reCAPTCHA Version.
What if the reCAPTCHA is invisible?
Add invisible=1 to your API request. The submit and poll flow is otherwise the same. Read How reCAPTCHA Invisible Works for details.
Does CaptchaAI support proxies for reCAPTCHA v2?
Yes. Add proxy (format: login:password@IP:PORT) and proxytype (HTTP, HTTPS, SOCKS4, or SOCKS5) to your request. Proxies are optional but recommended when IP matching matters.
Start solving reCAPTCHA v2
The fastest path from here:
- Get your API key — captchaai.com/api.php
- Copy the Python or Node.js code above — replace the placeholders with your key, sitekey, and pageurl
- Run it — you will have a solved token in under 60 seconds
- Inject the token — use direct field injection, callback, or request-body inclusion
- Explore the API docs — docs.captchaai.com for the full parameter reference
If you run into issues, start with the Troubleshooting section above or read the full reCAPTCHA v2 Errors and Fixes guide.
Iteration log
| Iteration | Focus | Changes |
|---|---|---|
| Draft 1 | Structure and content | Initial draft from brief — workflow, Python/Node.js code, FAQ, CTA |
| Draft 2 | Technical accuracy | Verified all API endpoints, parameters, and error codes against captchaai.com/api-docs. Added PHP example. Added proxy parameter note. |
| Draft 3 | Token injection depth | Added 3 injection methods (field, callback, request-body) with Selenium/Puppeteer code. Added error code table with official fix actions. |
| Draft 4 | SEO and internal linking | Added structured FAQ answers, internal links to 5 cluster articles, meta title/description, common mistakes table, technical stats table. |
| Draft 5 | Final QA polish | Verified code examples run cleanly. Added poll safety limit. Tightened intro. Added invisible reCAPTCHA boundary note. Confirmed all error codes match official docs. |
Visual asset brief
Hero image
- Alt text: Developer workflow for solving reCAPTCHA v2 via API — sitekey extraction, API request, token injection
- Must show: A clean technical flow from page → API → token → form submission
- File name: recaptcha-v2-api-solver-workflow-hero.png
In-article visual 1
- Placement: After "How the solving flow works"
- Type: Flowchart
- Alt text: Flowchart showing reCAPTCHA v2 API solving sequence — submit, poll, receive token, inject
- File name: recaptcha-v2-submit-poll-result-flow.png
In-article visual 2
- Placement: Before "Troubleshooting"
- Type: Decision tree
- Alt text: Decision tree for diagnosing reCAPTCHA v2 integration failures
- File name: recaptcha-v2-troubleshooting-decision-tree.png
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.