Solve reCAPTCHA Enterprise with Rust

By solvethree4 min read

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.
Fields used by the reCAPTCHA task
Fields used by the reCAPTCHA task
FieldWhat to send
siteThe target hostname. Replace accounts.example.com in the example.
actionThe action expected by the target. Replace login if necessary.
proxyThe single proxy URL read from SOLVETHREE_PROXY.
requestIdAn idempotency key containing 1–64 ASCII letters, numbers, underscores, or hyphens.
recaptchaTypeOmitted 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.

Cargo.toml · checked dependencies
[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"] }
Rust · bounded asynchronous REST calls
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. 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. 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. 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.

Run your first task.

Start with $5 in workspace credit.

Create a workspace