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
- Submit — POST to
https://ocr.captchaai.com/in.php→ receive task ID - 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 },
}
Async Solver (Recommended)
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(¶ms)
.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(¶ms)
.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.
Related Guides
- Solving CAPTCHAs with Go
- Solving CAPTCHAs with Scala
- Solving CAPTCHAs with Kotlin
- CaptchaAI API Documentation
Build blazing-fast CAPTCHA automation in Rust — get your API key and start solving.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.