Was this helpful?
How to Solve reCAPTCHA v2 Image Grid via Selenium
Technical engineer
Introduction
I used to think that automating reCAPTCHA v2 with an image grid was a headache due to dynamic tile loading and the need for computer vision. It turns out that combining Selenium with the 2Captcha Grid API solves this task in seconds.
In this guide, I will show you how to extract captcha data via the Canvas API, send it to platform workers, and programmatically click the correct images, avoiding all common pitfalls.
How It Works
reCAPTCHA v2 Image Grid presents a 3x3 or 4x4 grid and asks the user to select images matching a category. Instead of complex OCR, we use JavaScript (Canvas API) directly in the browser to "stitch" all tiles into a single base64 image. This file is sent to the Grid API, and the returned indices are used for clicks via Selenium.
Environment Setup
You will need:
- Python 3.7 or higher
- Selenium WebDriver (e.g., ChromeDriver)
- Requests library
- API key from 2Captcha
Install the required dependencies:
bash
pip install selenium requests
Step-by-Step Implementation
Step 1: Initialize WebDriver
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import requests
driver = webdriver.Chrome()
driver.maximize_window()
Step 2: Trigger Captcha on Target Site
python
driver.get("https://example.com")
wait = WebDriverWait(driver, 10)
checkbox = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".recaptcha-checkbox-border")))
checkbox.click()
# Wait for the grid to appear
time.sleep(2)
Step 3: Extract Data via Canvas API
This function retrieves the instruction text and merges the grid images into a single base64 object, correctly handling dynamically updated tiles in the 3x3 grid.
python
def extract_captcha_data(driver):
try:
instruction = driver.find_element(By.CSS_SELECTOR, ".rc-imageselect-desc-wrapper")
comment = instruction.text.replace('\n', ' ')
except Exception:
comment = "Select all matching images"
# Check for 4x4 grid
try:
img_4x4 = driver.find_element(By.CSS_SELECTOR, "img.rc-image-tile-44")
canvas_data = driver.execute_script("""
var img = arguments[0];
var canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return canvas.toDataURL().replace(/^data:image\/[A-z]*;base64,/, '');
""", img_4x4)
return {"body": canvas_data, "comment": comment, "rows": 4, "columns": 4}
except Exception:
pass
# Handle 3x3 grid with updated tiles
try:
table_3x3 = driver.find_element(By.CSS_SELECTOR, "table.rc-imageselect-table-33 tbody")
initial_img = table_3x3.find_element(By.CSS_SELECTOR, "img.rc-image-tile-33")
updated_tiles = driver.find_elements(By.CSS_SELECTOR, "img.rc-image-tile-11")
canvas_data = driver.execute_script("""
var initialImg = arguments[0];
var updatedTiles = arguments[1];
var canvas = document.createElement('canvas');
canvas.width = initialImg.naturalWidth;
canvas.height = initialImg.naturalHeight;
var ctx = canvas.getContext('2d');
ctx.drawImage(initialImg, 0, 0);
if (updatedTiles.length > 0) {
var positions = [
{x: 0, y: 0}, {x: canvas.width/3, y: 0}, {x: canvas.width/3*2, y: 0},
{x: 0, y: canvas.height/3}, {x: canvas.width/3, y: canvas.height/3}, {x: canvas.width/3*2, y: canvas.height/3},
{x: 0, y: canvas.height/3*2}, {x: canvas.width/3, y: canvas.height/3*2}, {x: canvas.width/3*2, y: canvas.height/3*2}
];
updatedTiles.forEach(function(tile) {
var index = tile.parentElement.parentElement.parentElement.tabIndex - 3;
if (index >= 1 && index <= 9) {
ctx.drawImage(tile, positions[index-1].x, positions[index-1].y);
}
});
}
return canvas.toDataURL().replace(/^data:image\/[A-z]*;base64,/, '');
""", initial_img, updated_tiles)
return {"body": canvas_data, "comment": comment, "rows": 3, "columns": 3}
except Exception as e:
raise Exception(f"Failed to extract captcha data: {e}")
Step 4: Send to Grid API (API v2)
python
def solve_captcha_via_api(captcha_data, api_key):
api_url = "https://api.2Captcha.com/createTask"
payload = {
"clientKey": api_key,
"task": {
"type": "GridTask",
"body": captcha_data["body"],
"comment": captcha_data["comment"],
"rows": captcha_data["rows"],
"columns": captcha_data["columns"]
}
}
response = requests.post(api_url, json=payload)
result = response.json()
if result.get("errorId") != 0:
raise Exception(f"API Error: {result.get('errorDescription', 'Unknown error')}")
task_id = result.get("taskId")
get_result_url = "https://api.2Captcha.com/getTaskResult"
while True:
time.sleep(5)
result_response = requests.post(
get_result_url,
json={"clientKey": api_key, "taskId": task_id}
)
result_data = result_response.json()
if result_data.get("status") == "ready":
# API v2 returns an array of integers, e.g., [1, 4, 7]
return result_data["solution"]["click"], task_id
if result_data.get("status") == "processing":
continue
raise Exception(f"API Error: {result_data.get('errorDescription')}")
Step 5: Handle Clicks
Important: The API returns 1-based indices, while DOM selectors use 0-based indices.
python
def click_on_images(driver, indices):
tasks = driver.find_elements(By.CSS_SELECTOR, "div.rc-imageselect-tile")
for index in indices:
if 1 <= index <= len(tasks):
tasks[index - 1].click()
time.sleep(0.3) # Delay to simulate human behavior
try:
verify_button = driver.find_element(By.CSS_SELECTOR, ".rc-button-default")
verify_button.click()
except Exception:
pass # Button may be absent if auto-verify is enabled
Step 6: Full Usage Cycle
python
def main():
API_KEY = "YOUR_API_KEY"
try:
driver = webdriver.Chrome()
driver.maximize_window()
driver.get("https://example.com")
wait = WebDriverWait(driver, 10)
checkbox = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".recaptcha-checkbox-border")))
checkbox.click()
time.sleep(2)
captcha_data = extract_captcha_data(driver)
print(f"Extracted data: {captcha_data['rows']}x{captcha_data['columns']} grid")
solution_indices, task_id = solve_captcha_via_api(captcha_data, API_KEY)
print(f"Solution from workers: {solution_indices}")
click_on_images(driver, solution_indices)
time.sleep(5)
except Exception as e:
print(f"Error: {e}")
finally:
input("Press Enter to close the browser...")
driver.quit()
if __name__ == "__main__":
main()
Request Parameters (createTask)
| Parameter | Type | Required | Description |
|---|---|---|---|
clientKey |
String | Yes | Your API key |
type |
String | Yes | Task type: GridTask |
body |
String | Yes | Image in base64 format |
comment |
String | Yes | Instruction text in English |
rows |
Number | Yes | Number of rows (3 or 4) |
columns |
Number | Yes | Number of columns (3 or 4) |
Troubleshooting
Grid does not appear
Ensure the checkbox was successfully clicked. Use an explicit wait:
python
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".rc-imageselect-table-33, .rc-image-tile-44"))
)
Incorrect image selection
Verify the index conversion logic: the API returns values starting from 1, while Python/JS lists start from 0. The index - 1 formula in the code above solves this.
Canvas extraction error
Ensure the page has fully loaded. Increase the wait time before calling extract_captcha_data or verify the selectors in the target site's DevTools.
Reporting Accuracy
If the website rejects the solution, report it to get a refund. If the solution is successful, send a positive report.
python
def report_incorrect(task_id, api_key):
requests.get(f"https://2Captcha.com/res.php?key={api_key}&action=reportbad&id={task_id}")
def report_correct(task_id, api_key):
requests.get(f"https://2Captcha.com/res.php?key={api_key}&action=reportgood&id={task_id}")
Useful Links
- Grid API Documentation: https://2captcha.com/api-docs/grid
- Full Selenium example repository
- Testing Sandbox: https://2captcha.com/setting#sandbox
Conclusion
Automating reCAPTCHA v2 Image Grid comes down to properly extracting the DOM state via Canvas and correctly converting indices when clicking. The provided code accounts for the dynamic replacement of 3x3 tiles, which is the most common cause of errors. Start with a test run on a demo page, verify the base64 extraction logs, and the integration will take minimal time.