API Tutorials

Solving CAPTCHAs with Rust and CaptchaAI API

Rust's safety guarantees, zero-cost abstractions, and async ecosystem make it increasingly popular for high-performance scrapers and automation tools. When those tools hit CAPTCHAs, CaptchaAI's HTTP API integrates cleanly through reqwest and tokio.

This guide covers reCAPTCHA v2, Cloudflare Turnstile, and image CAPTCHA solving — with both synchronous (blocking) and async implementations you can embed in any Rust project.


Why Rust for CAPTCHA Automation

  • Memory safety without GC — no crashes, no leaks at scale
  • Async native — tokio + reqwest handle thousands of concurrent solves
  • Type safety — serde serialization catches API errors at compile time
  • Performance — ideal for high-throughput CAPTCHA pipelines
  • Cross-platform — single binary deploys to Linux, macOS, Windows

Prerequisites

Add dependencies to Cargo.toml:

[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
thiserror = "2"

CaptchaAI API Flow

  1. Submit — POST to https://ocr.captchaai.com/in.php → receive task ID
  2. Poll — GET https://ocr.captchaai.com/res.php?action=get&id=TASK_ID → receive token

Type Definitions

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Deserialize)]
struct ApiResponse {
    status: u8,
    request: String,
}

#[derive(Debug, Error)]
enum CaptchaError {
    #[error("API error: {0}")]
    ApiError(String),
    #[error("HTTP error: {0}")]
    HttpError(#[from] reqwest::Error),
    #[error("Timeout waiting for solution")]
    Timeout,
    #[error("Invalid response: {0}")]
    ParseError(String),
}

#[derive(Debug, Clone)]
enum CaptchaType {
    RecaptchaV2 { sitekey: String, page_url: String },
    RecaptchaV3 { sitekey: String, page_url: String, action: String, min_score: f32 },
    Turnstile { sitekey: String, page_url: String },
    ImageBase64 { body: String },
}

use reqwest::Client;
use std::time::Duration;

struct CaptchaSolver {
    api_key: String,
    client: Client,
    base_url: String,
    poll_interval: Duration,
    max_wait: Duration,
}

impl CaptchaSolver {
    fn new(api_key: &str) -> Self {
        Self {
            api_key: api_key.to_string(),
            client: Client::builder()
                .timeout(Duration::from_secs(30))
                .build()
                .expect("Failed to create HTTP client"),
            base_url: "https://ocr.captchaai.com".to_string(),
            poll_interval: Duration::from_secs(5),
            max_wait: Duration::from_secs(300),
        }
    }

    async fn solve(&self, captcha: CaptchaType) -> Result<String, CaptchaError> {
        let task_id = self.submit(captcha).await?;
        self.poll(&task_id).await
    }

    async fn submit(&self, captcha: CaptchaType) -> Result<String, CaptchaError> {
        let mut params = vec![
            ("key".to_string(), self.api_key.clone()),
            ("json".to_string(), "1".to_string()),
        ];

        match captcha {
            CaptchaType::RecaptchaV2 { sitekey, page_url } => {
                params.push(("method".to_string(), "userrecaptcha".to_string()));
                params.push(("googlekey".to_string(), sitekey));
                params.push(("pageurl".to_string(), page_url));
            }
            CaptchaType::RecaptchaV3 { sitekey, page_url, action, min_score } => {
                params.push(("method".to_string(), "userrecaptcha".to_string()));
                params.push(("googlekey".to_string(), sitekey));
                params.push(("pageurl".to_string(), page_url));
                params.push(("version".to_string(), "v3".to_string()));
                params.push(("action".to_string(), action));
                params.push(("min_score".to_string(), min_score.to_string()));
            }
            CaptchaType::Turnstile { sitekey, page_url } => {
                params.push(("method".to_string(), "turnstile".to_string()));
                params.push(("key".to_string(), sitekey));
                params.push(("pageurl".to_string(), page_url));
            }
            CaptchaType::ImageBase64 { body } => {
                params.push(("method".to_string(), "base64".to_string()));
                params.push(("body".to_string(), body));
            }
        }

        let response: ApiResponse = self.client
            .post(format!("{}/in.php", self.base_url))
            .form(&params)
            .send()
            .await?
            .json()
            .await?;

        if response.status != 1 {
            return Err(CaptchaError::ApiError(response.request));
        }

        Ok(response.request)
    }

    async fn poll(&self, task_id: &str) -> Result<String, CaptchaError> {
        let start = std::time::Instant::now();

        loop {
            if start.elapsed() > self.max_wait {
                return Err(CaptchaError::Timeout);
            }

            tokio::time::sleep(self.poll_interval).await;

            let response: ApiResponse = self.client
                .get(format!("{}/res.php", self.base_url))
                .query(&[
                    ("key", self.api_key.as_str()),
                    ("action", "get"),
                    ("id", task_id),
                    ("json", "1"),
                ])
                .send()
                .await?
                .json()
                .await?;

            if response.request == "CAPCHA_NOT_READY" {
                continue;
            }

            if response.status != 1 {
                return Err(CaptchaError::ApiError(response.request));
            }

            return Ok(response.request);
        }
    }

    async fn check_balance(&self) -> Result<f64, CaptchaError> {
        let response: ApiResponse = self.client
            .get(format!("{}/res.php", self.base_url))
            .query(&[
                ("key", self.api_key.as_str()),
                ("action", "getbalance"),
                ("json", "1"),
            ])
            .send()
            .await?
            .json()
            .await?;

        response.request.parse::<f64>()
            .map_err(|e| CaptchaError::ParseError(e.to_string()))
    }
}

Usage

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let solver = CaptchaSolver::new("YOUR_API_KEY");

    // Check balance
    let balance = solver.check_balance().await?;
    println!("Balance: ${:.2}", balance);

    // Solve reCAPTCHA v2
    let token = solver.solve(CaptchaType::RecaptchaV2 {
        sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-".to_string(),
        page_url: "https://example.com/login".to_string(),
    }).await?;

    println!("Token: {}...", &token[..50.min(token.len())]);

    // Solve Turnstile
    let turnstile_token = solver.solve(CaptchaType::Turnstile {
        sitekey: "0x4AAAAAAAB5...".to_string(),
        page_url: "https://example.com/form".to_string(),
    }).await?;

    println!("Turnstile: {}...", &turnstile_token[..50.min(turnstile_token.len())]);

    Ok(())
}

Solving Image CAPTCHAs

use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use std::fs;

async fn solve_image_captcha(
    solver: &CaptchaSolver,
    image_path: &str,
) -> Result<String, CaptchaError> {
    let image_bytes = fs::read(image_path)
        .map_err(|e| CaptchaError::ParseError(e.to_string()))?;
    let encoded = STANDARD.encode(&image_bytes);

    solver.solve(CaptchaType::ImageBase64 { body: encoded }).await
}

// Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let solver = CaptchaSolver::new("YOUR_API_KEY");
    let text = solve_image_captcha(&solver, "captcha.png").await?;
    println!("Image text: {}", text);
    Ok(())
}

Concurrent Solving with Tokio

Solve multiple CAPTCHAs in parallel:

use futures::future::join_all;

async fn solve_batch(
    solver: &CaptchaSolver,
    tasks: Vec<CaptchaType>,
) -> Vec<Result<String, CaptchaError>> {
    let futures: Vec<_> = tasks.into_iter()
        .map(|task| solver.solve(task))
        .collect();

    join_all(futures).await
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let solver = CaptchaSolver::new("YOUR_API_KEY");

    let tasks = vec![
        CaptchaType::RecaptchaV2 {
            sitekey: "KEY_A".to_string(),
            page_url: "https://site-a.com".to_string(),
        },
        CaptchaType::RecaptchaV2 {
            sitekey: "KEY_B".to_string(),
            page_url: "https://site-b.com".to_string(),
        },
        CaptchaType::Turnstile {
            sitekey: "KEY_C".to_string(),
            page_url: "https://site-c.com".to_string(),
        },
    ];

    let results = solve_batch(&solver, tasks).await;

    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(token) => println!("Task {}: {:.50}...", i, token),
            Err(e) => eprintln!("Task {}: {}", i, e),
        }
    }

    Ok(())
}

Error Handling with Retry

use std::time::Duration;

async fn solve_with_retry(
    solver: &CaptchaSolver,
    captcha: CaptchaType,
    max_retries: u32,
) -> Result<String, CaptchaError> {
    let retryable = |err: &CaptchaError| -> bool {
        match err {
            CaptchaError::ApiError(msg) => {
                msg.contains("ERROR_NO_SLOT_AVAILABLE")
                    || msg.contains("ERROR_CAPTCHA_UNSOLVABLE")
            }
            CaptchaError::Timeout => true,
            CaptchaError::HttpError(_) => true,
            _ => false,
        }
    };

    let mut last_error = CaptchaError::Timeout;

    for attempt in 0..=max_retries {
        if attempt > 0 {
            let delay = Duration::from_secs(2u64.pow(attempt) + rand::random::<u64>() % 3);
            eprintln!("Retry {}/{} after {:?}", attempt, max_retries, delay);
            tokio::time::sleep(delay).await;
        }

        match solver.solve(captcha.clone()).await {
            Ok(token) => return Ok(token),
            Err(e) if retryable(&e) => {
                last_error = e;
                continue;
            }
            Err(e) => return Err(e),
        }
    }

    Err(last_error)
}

Submitting Solved Tokens

use reqwest::Client;
use std::collections::HashMap;

async fn submit_form_with_token(
    url: &str,
    token: &str,
    form_data: HashMap<&str, &str>,
) -> Result<String, reqwest::Error> {
    let client = Client::new();

    let mut params = form_data;
    params.insert("g-recaptcha-response", token);

    let response = client
        .post(url)
        .form(&params)
        .send()
        .await?;

    response.text().await
}

// Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let solver = CaptchaSolver::new("YOUR_API_KEY");

    let token = solver.solve(CaptchaType::RecaptchaV2 {
        sitekey: "SITEKEY".to_string(),
        page_url: "https://example.com/login".to_string(),
    }).await?;

    let mut form = HashMap::new();
    form.insert("username", "user@example.com");
    form.insert("password", "password123");

    let result = submit_form_with_token(
        "https://example.com/login",
        &token,
        form,
    ).await?;

    println!("Response: {}", &result[..200.min(result.len())]);
    Ok(())
}

Troubleshooting

Error Cause Fix
ERROR_WRONG_USER_KEY Invalid API key Verify key at dashboard
ERROR_ZERO_BALANCE No funds Top up account
ERROR_NO_SLOT_AVAILABLE Server busy Retry after 5 seconds
reqwest::Error — TLS Certificate issues Update rustls or use native-tls feature
Compile error on clone() CaptchaType not Clone Add #[derive(Clone)] to enum
Slow polling Default interval too short Increase poll_interval to 5-10s

FAQ

Does CaptchaAI have a Rust crate?

CaptchaAI uses a REST API that works with any HTTP client. The reqwest + serde combination shown here gives you idiomatic Rust integration.

Should I use async or blocking?

Use async (tokio + reqwest) for any production use. The blocking API is fine for simple CLI tools but can't handle concurrent solves efficiently.

How many CAPTCHAs can I solve concurrently?

Tokio can handle thousands of concurrent futures. CaptchaAI's API is the bottleneck — start with 10-20 concurrent solves and monitor response times.

Can I use this in a web server (Actix, Axum)?

Yes. The CaptchaSolver is Send + Sync and works inside Actix or Axum handlers. Wrap it in Arc for shared ownership across handlers.



Build blazing-fast CAPTCHA automation in Rust — get your API key and start solving.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.