Tutorials 10 min read

Solving CAPTCHAs with Python Requests: Complete Tutorial

Build a Python CAPTCHA solver using the requests library. Full tutorial with createTask, polling, error handling, and a reusable helper class.

Solving CAPTCHAs with Python Requests: Complete Tutorial

A Python CAPTCHA solver built on the requests library is the fastest way to add automated CAPTCHA solving to your scripts, scrapers, and data pipelines. This tutorial builds a reusable UCaptchaClient class from scratch, demonstrates solving a reCAPTCHA v2 challenge, and covers the error handling patterns you need for reliable production use.

If you are new to CAPTCHA solver APIs in general, read the CAPTCHA Solver API Integration Tutorial first for the full conceptual overview.

Prerequisites

You need Python 3.8+ and the requests library:

pip install requests

Set your uCaptcha API key as an environment variable:

export UCAPTCHA_KEY="ucap_your_key_here"

The UCaptchaClient Class

Rather than writing one-off HTTP calls everywhere, wrap the API in a class you can import into any project. Here is the complete implementation:

import os
import time
import logging
from typing import Optional

import requests
from requests.exceptions import RequestException

logger = logging.getLogger(__name__)


class UCaptchaError(Exception):
    """Raised when the uCaptcha API returns an error."""
    def __init__(self, code: str, description: str = ""):
        self.code = code
        self.description = description
        super().__init__(f"{code}: {description}")


class UCaptchaClient:
    """Reusable client for the uCaptcha CAPTCHA solving API."""

    API_URL = "https://api.ucaptcha.net"
    RETRYABLE_ERRORS = {"ERROR_NO_SLOT_AVAILABLE"}

    def __init__(
        self,
        client_key: Optional[str] = None,
        max_retries: int = 3,
        poll_interval: int = 4,
        timeout: int = 120,
    ):
        self.client_key = client_key or os.environ["UCAPTCHA_KEY"]
        self.max_retries = max_retries
        self.poll_interval = poll_interval
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({"Content-Type": "application/json"})

    def _post(self, endpoint: str, payload: dict) -> dict:
        """Make a POST request with retry logic."""
        url = f"{self.API_URL}/{endpoint}"
        last_error = None

        for attempt in range(self.max_retries):
            try:
                resp = self.session.post(url, json=payload, timeout=15)
                resp.raise_for_status()
                data = resp.json()

                if data.get("errorId", 0) != 0:
                    code = data.get("errorCode", "UNKNOWN")
                    desc = data.get("errorDescription", "")
                    if code in self.RETRYABLE_ERRORS:
                        wait = 3 * (attempt + 1)
                        logger.warning(f"{code} on attempt {attempt + 1}, retrying in {wait}s")
                        time.sleep(wait)
                        continue
                    raise UCaptchaError(code, desc)

                return data

            except RequestException as e:
                last_error = e
                if attempt < self.max_retries - 1:
                    time.sleep(2 * (attempt + 1))

        raise last_error or Exception("Max retries exceeded")

    def create_task(self, task: dict) -> str:
        """Submit a CAPTCHA task and return the task ID."""
        data = self._post("createTask", {
            "clientKey": self.client_key,
            "task": task,
        })
        task_id = data["taskId"]
        logger.info(f"Created task {task_id}")
        return task_id

    def get_result(self, task_id: str) -> dict:
        """Poll for a task result until ready or timeout."""
        deadline = time.time() + self.timeout
        time.sleep(5)  # initial wait before first poll

        while time.time() < deadline:
            data = self._post("getTaskResult", {
                "clientKey": self.client_key,
                "taskId": task_id,
            })

            if data.get("status") == "ready":
                logger.info(f"Task {task_id} solved")
                return data["solution"]

            time.sleep(self.poll_interval)

        raise TimeoutError(f"Task {task_id} timed out after {self.timeout}s")

    def solve(self, task: dict) -> dict:
        """Create a task and wait for the result in one call."""
        task_id = self.create_task(task)
        return self.get_result(task_id)

    def get_balance(self) -> float:
        """Check account balance."""
        data = self._post("getBalance", {"clientKey": self.client_key})
        return data["balance"]

The class gives you three levels of usage:

  • solve(task) — One-liner. Pass the task dict, get the solution back.
  • create_task(task) + get_result(task_id) — Two-step when you want to do other work while waiting.
  • get_balance() — Monitor your funds programmatically.

Solving a reCAPTCHA v2

Here is how you solve a reCAPTCHA v2 using the client:

client = UCaptchaClient()

solution = client.solve({
    "type": "RecaptchaV2TaskProxyless",
    "websiteURL": "https://example.com/login",
    "websiteKey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
})

recaptcha_token = solution["gRecaptchaResponse"]
print(f"Token: {recaptcha_token[:50]}...")

You then include recaptcha_token in the form submission to the target site, typically as the g-recaptcha-response POST parameter:

login_resp = requests.post("https://example.com/login", data={
    "username": "user@example.com",
    "password": "hunter2",
    "g-recaptcha-response": recaptcha_token,
})

Solving Other CAPTCHA Types

The solve() method works with any task type. Change the type and fields:

hCaptcha:

solution = client.solve({
    "type": "HardCaptchaTaskProxyless",
    "websiteURL": "https://example.com/signup",
    "websiteKey": "a5f74b19-9e45-40e0-b45d-47ff91b7a6c2",
})
token = solution["gRecaptchaResponse"]  # hCaptcha uses the same field name

Cloudflare Turnstile:

solution = client.solve({
    "type": "TurnstileTaskProxyless",
    "websiteURL": "https://example.com",
    "websiteKey": "0x4AAAAAAABS7vwvV6VFfMcD",
})
token = solution["token"]

Image to text:

import base64

with open("captcha.png", "rb") as f:
    b64_image = base64.b64encode(f.read()).decode()

solution = client.solve({
    "type": "ImageToTextTask",
    "body": b64_image,
})
text = solution["text"]

Error Handling in Practice

The UCaptchaClient already handles retryable errors and network failures internally. But your calling code should still handle the exceptions the class raises:

from ucaptcha_client import UCaptchaClient, UCaptchaError

client = UCaptchaClient(timeout=90)

try:
    solution = client.solve({
        "type": "RecaptchaV2TaskProxyless",
        "websiteURL": "https://example.com/login",
        "websiteKey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    })
    token = solution["gRecaptchaResponse"]

except UCaptchaError as e:
    if e.code == "ERROR_ZERO_BALANCE":
        print("Account balance is empty. Top up at ucaptcha.net")
    elif e.code == "ERROR_CAPTCHA_UNSOLVABLE":
        print("CAPTCHA could not be solved. Retrying with a new task...")
        # Retry logic here
    else:
        print(f"API error: {e}")

except TimeoutError:
    print("Solve timed out. The CAPTCHA may be unusually complex.")

except Exception as e:
    print(f"Unexpected error: {e}")

The key errors to watch for:

  • ERROR_ZERO_BALANCE — Stop processing and alert. No point retrying.
  • ERROR_CAPTCHA_UNSOLVABLE — The CAPTCHA image or parameters were invalid, or no solver could crack it. Retrying with a fresh task often works.
  • ERROR_NO_SLOT_AVAILABLE — Temporary capacity issue. The client retries this automatically.
  • TimeoutError — The solve took longer than your timeout. Increase the timeout or investigate whether the CAPTCHA parameters are correct.

Async Alternative with aiohttp

If your project is async, swap requests for aiohttp. The structure is identical:

import aiohttp
import asyncio

async def solve_async(task: dict) -> dict:
    async with aiohttp.ClientSession() as session:
        # Create task
        async with session.post(
            "https://api.ucaptcha.net/createTask",
            json={"clientKey": os.environ["UCAPTCHA_KEY"], "task": task},
        ) as resp:
            data = await resp.json()
            task_id = data["taskId"]

        # Poll
        await asyncio.sleep(5)
        while True:
            async with session.post(
                "https://api.ucaptcha.net/getTaskResult",
                json={"clientKey": os.environ["UCAPTCHA_KEY"], "taskId": task_id},
            ) as resp:
                data = await resp.json()
                if data.get("status") == "ready":
                    return data["solution"]
            await asyncio.sleep(4)

This lets you solve multiple CAPTCHAs concurrently with asyncio.gather(), which is useful for high-throughput scraping.

Putting It All Together

Save the UCaptchaClient class as ucaptcha_client.py in your project, then import it wherever you need CAPTCHA solving:

from ucaptcha_client import UCaptchaClient

client = UCaptchaClient()

# Check balance on startup
balance = client.get_balance()
print(f"Balance: ${balance:.2f}")

# Solve as needed
solution = client.solve({
    "type": "RecaptchaV2TaskProxyless",
    "websiteURL": "https://target-site.com/page",
    "websiteKey": "SITE_KEY_HERE",
})

The class handles retries, timeouts, and error classification so your business logic stays clean. For the full conceptual overview of the API, including callback-based workflows and routing configuration, see the CAPTCHA Solver API Integration Tutorial.

uCaptcha routes your tasks across six providers to optimize for speed, cost, or reliability depending on your routing configuration. The Python client above works the same regardless of which provider ultimately solves the CAPTCHA — that routing is handled server-side, transparently. Get your API key at ucaptcha.net and start solving in minutes.

Related Articles