Solve reCAPTCHA Enterprise with Rust
Use solvethree’s REST API from Rust: create a task, poll until it’s ready, then submit solution.token to the target using the returned browser identity and the same proxy. The example below uses reqwest, Tokio, and serde_json directly—not an official Rust SDK.
On this page
Configure the site and request
First, create an active profile for the target hostname in your workspace’s console. Sending a hostname and action doesn’t create one. See site profile setup if the target isn’t configured yet.
Set these environment variables on the server where the Rust client will run:
SOLVETHREE_API_KEY: your workspace API key. Keep it server-side.SOLVETHREE_PROXY: the proxy URL for solving and the subsequent target request.REQUEST_ID: an optional, stable ID for this logical task. Save it before sending the request so you can recover after an interrupted connection.
| Field | What to send |
|---|---|
site | The target hostname. Replace accounts.example.com in the example. |
action | The action expected by the target. Replace login if necessary. |
proxy | The single proxy URL read from SOLVETHREE_PROXY. |
requestId | An idempotency key containing 1–64 ASCII letters, numbers, underscores, or hyphens. |
recaptchaType | Omitted here because it defaults to enterprise-v3. |
Don’t add type or url fields to this reCAPTCHA request. Exactly one of proxy or proxyCandidates is required; the example uses proxy so the target handoff can use that same connection.
Create the Rust client
Place the dependency file in Cargo.toml and the client in src/main.rs of a Cargo binary project. After setting the environment variables and changing the target hostname and action, run the project with Cargo. The dependencies pin reqwest to version 0.13.5.
[package]
name = "solvethree-example"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "=0.13.5", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }use std::{env, error::Error, time::{Duration, SystemTime, UNIX_EPOCH}};
use reqwest::{Client, redirect::Policy};
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let api = "https://api.solvethree.com/v1";
let key = env::var("SOLVETHREE_API_KEY")?;
let request_id = env::var("REQUEST_ID").unwrap_or_else(|_| format!("rust-{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
let input = json!({"site":"accounts.example.com", "action":"login", "proxy":env::var("SOLVETHREE_PROXY")?, "requestId":request_id});
let client = Client::builder().redirect(Policy::none()).timeout(Duration::from_secs(30)).build()?;
let mut task_id: Option<String> = None;
let outcome = tokio::time::timeout(Duration::from_secs(180), async {
let response = client.post(format!("{api}/tasks?waitMs=0")).bearer_auth(&key).json(&input).send().await?;
if !response.status().is_success() { return Err(format!("API HTTP {}", response.status()).into()); }
let mut result: Value = response.json().await?;
let id = result["id"].as_str().ok_or("Missing task ID")?.to_owned();
if !id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') { return Err("Invalid task ID".into()); }
task_id = Some(id.clone());
loop {
match result["status"].as_str() {
Some("ready") => return Ok::<Value, Box<dyn Error>>(result),
Some("failed") => return Err(format!("Task failed: {}", result["errorCode"]).into()),
Some("pending") | Some("solving") => {},
_ => return Err("Unexpected task status".into()),
}
tokio::time::sleep(Duration::from_secs(1)).await;
let response = client.get(format!("{api}/tasks/{id}?waitMs=15000")).bearer_auth(&key).send().await?;
if !response.status().is_success() { return Err(format!("API HTTP {}", response.status()).into()); }
result = response.json().await?;
}
}).await;
match outcome {
Ok(Ok(result)) => {
println!("Ready task: {}", result["id"].as_str().unwrap_or("unknown"));
// Submit result["solution"]["token"] with returned browser headers
// through the original proxy; do not log the solution.
Ok(())
},
Ok(Err(error)) => {
eprintln!("Recovery requestId={request_id} taskId={task_id:?}");
Err(error)
},
Err(_) => {
eprintln!("Recovery requestId={request_id} taskId={task_id:?}");
Err("Local deadline reached; recover the existing task".into())
}
}
}The client makes one create request to POST /v1/tasks, authenticating with a Bearer token. It retains the returned task ID and uses GET /v1/tasks/:id for subsequent status requests.
Two limits keep the client from waiting indefinitely: reqwest’s timeout limits each HTTP request to 30 seconds, while Tokio’s timeout bounds creation and polling together to 180 seconds. Those are client-side choices, not service completion guarantees. Redirects are disabled through reqwest’s ClientBuilder.
Only pending and solving continue the polling loop. ready returns the result, failed stops with an error, and an unexpected status also stops rather than polling forever. The success path prints the task ID, not the token.
Recover an interrupted request without another solve
The example keeps requestId and the returned task ID in memory and prints them when an HTTP, transport, parsing, or deadline error occurs. It doesn’t persist them across process termination. For restart recovery, store the request ID and original task inputs before creation, then store the task ID as soon as it arrives.
- 1.
If you have a task ID, retrieve that task
Resume with GET /v1/tasks/:id rather than creating another task. The sample’s normal entry point always creates; a restart path should use the saved ID.
- 2.
If you only have a request ID, recover creation
Submit the same task request with that same requestId. It returns the existing task. Changing the inputs while reusing the ID causes a conflict.
- 3.
Handle the recovered status
Continue polling pending or solving tasks. Use a ready solution, or stop on failed and inspect errorCode and error. A local timeout alone doesn’t establish that the remote task failed.
HTTP failures and task failures need different handling. The client checks for non-success HTTP responses before parsing a success payload. A terminal task failure instead arrives as task status failed; failed solves are refunded. The sample’s failure message includes errorCode only, so also inspect error in the task response when diagnosing it. See API error handling before retrying unchanged inputs.
Submit the token with its browser identity
The example stops at a ready task; it doesn’t send the application request to the target. That request needs solution.token, not the task ID. Use the returned userAgent and secChUa identity through the original proxy, and submit to the intended target rather than mixing values from another browser session.
A ready task means a solution was generated, not that the target accepted your application request. Add the target-specific submission at the marked success-path comment; see the reCAPTCHA Enterprise integration guide for token handoff guidance.
Sources and API reference
Run your first task.
Start with $5 in workspace credit.