Captcha bypass tutorials

Was this helpful?

How to Solve reCAPTCHA v2 Image Grid via Selenium

Nick McBain
Nick McBain

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 Copy
pip install selenium requests

Step-by-Step Implementation

Step 1: Initialize WebDriver

python Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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}")

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.