Use the CAPTCHA API with Selenium in Java

By solvethree5 min read

Request the token first, then configure Chrome with the same proxy and the returned userAgent and secChUa before navigating. Put solution.token into your application's CAPTCHA field, submit the form, and wait for the application's success state—not just a ready task.

The examples use a Java REST helper and Selenium ChromeDriver. There's no automatic CAPTCHA detection or browser plugin involved.

On this page

Prepare the Java project and target site

  1. 1.

    Configure the project

    Use Java 21 or later. In a Maven project, set the Java compilation level to 21 and add Gson 2.15.0 and org.seleniumhq.selenium:selenium-java:4.52.0. Configure Chrome and ChromeDriver using the Selenium Chrome documentation.

  2. 2.

    Save both Java files

    Save the first example as src/main/java/Solve.java and the second as src/main/java/BrowserSolve.java. Keep them in the same package; the supplied files have no package declaration. BrowserSolve calls Solve.api, not Solve.main.

  3. 3.

    Activate the site profile

    Set up the target hostname in the solvethree console and make sure its profile is active for your workspace. Sending a hostname and action doesn't create a profile. See site profile setup.

  4. 4.

    Set the environment

    Configure the three environment variables below in the process that will run BrowserSolve. Keep the workspace API key in your trusted Java process, not in the page or browser JavaScript.

Environment variables for BrowserSolve
Environment variables for BrowserSolve
VariableValue
SOLVETHREE_API_KEYYour workspace API key.
SOLVETHREE_PROXYAn IP-allowlisted HTTP proxy URL without embedded username or password, path, query, or fragment.
TARGET_URLThe full URL of the application page containing the form.

This example requires an IP-allowlisted proxy because it doesn't support passing proxy credentials through Chrome's proxy URL. The same proxy value goes to the task API and Chrome.

Add Solve.java for API requests

Solve.api sends authenticated requests to https://api.solvethree.com/v1, checks the HTTP status before parsing a success response, and limits each request to the remaining local deadline.

Java 21+ · Solve.java (Gson dependency)
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;

public class Solve {
    static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(15)).build();
    static final String KEY = System.getenv("SOLVETHREE_API_KEY");

    static JsonObject api(String path, JsonObject body, long deadline) throws Exception {
        long remaining = deadline - System.nanoTime();
        if (remaining <= 0) throw new IllegalStateException("Keep the task ID and resume polling");
        var builder = HttpRequest.newBuilder(URI.create("https://api.solvethree.com/v1" + path))
            .timeout(Duration.ofNanos(Math.min(remaining, Duration.ofSeconds(30).toNanos())))
            .header("Authorization", "Bearer " + KEY)
            .header("Content-Type", "application/json");
        if (body == null) builder.GET();
        else builder.POST(HttpRequest.BodyPublishers.ofString(body.toString()));
        var response = HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300)
            throw new IllegalStateException("API HTTP " + response.statusCode());
        return JsonParser.parseString(response.body()).getAsJsonObject();
    }
    public static void main(String[] args) throws Exception {
        String proxy = System.getenv("SOLVETHREE_PROXY");
        if (KEY == null || proxy == null) throw new IllegalArgumentException("Set the API key and proxy");
        var input = new JsonObject();
        input.addProperty("site", "accounts.example.com");
        input.addProperty("action", "login");
        input.addProperty("proxy", proxy);
        input.addProperty("requestId", UUID.randomUUID().toString());
        // Save input before creation if the task must be recovered after restart.
        long deadline = System.nanoTime() + Duration.ofSeconds(180).toNanos();
        var task = api("/tasks?waitMs=0", input, deadline);
        while (task.get("status").getAsString().equals("pending") || task.get("status").getAsString().equals("solving")) {
            String id = URLEncoder.encode(task.get("id").getAsString(), StandardCharsets.UTF_8);
            task = api("/tasks/" + id + "?waitMs=15000", null, deadline);
            if (task.get("status").getAsString().equals("pending") || task.get("status").getAsString().equals("solving"))
                Thread.sleep(1000);
        }
        if (!task.get("status").getAsString().equals("ready"))
            throw new IllegalStateException("Task failed: " + task.get("errorCode")
                + ": " + task.get("error"));
        var solution = task.getAsJsonObject("solution");
        // Submit solution.token with userAgent/secChUa and the original proxy.
        System.out.println("Ready task: " + task.get("id").getAsString());
    }
}

The standalone main method demonstrates task creation and polling. Its accounts.example.com hostname and login action are placeholders. For the Selenium integration, use this file as a helper and run BrowserSolve instead; running both main methods would create separate tasks.

A reCAPTCHA request needs a hostname in site, the application's action, and exactly one of proxy or proxyCandidates. Don't add type or url. Omitting recaptchaType selects enterprise-v3, which is what these examples use.

Start Chrome with the ready solution's identity

BrowserSolve derives site from TARGET_URL, creates the task, and polls while its status is pending or solving. Only a ready task supplies the solution used by the browser.

Java · pass a ready solution to Chrome
// Save as BrowserSolve.java beside Solve.java from the Java REST quickstart.
// Add Selenium Java 4.52.0 and Gson 2.15.0 to your Maven dependencies; Java 21+.
import com.google.gson.JsonObject;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class BrowserSolve {
    public static void main(String[] args) throws Exception {
        String target = System.getenv("TARGET_URL");
        String proxy = System.getenv("SOLVETHREE_PROXY");
        URI proxyUri = URI.create(proxy);
        if (proxyUri.getUserInfo() != null) throw new IllegalArgumentException("Use an IP-allowlisted proxy");
        var input = new JsonObject();
        input.addProperty("site", URI.create(target).getHost());
        input.addProperty("action", "login");
        input.addProperty("proxy", proxy);
        input.addProperty("requestId", UUID.randomUUID().toString());
        long deadline = System.nanoTime() + Duration.ofSeconds(180).toNanos();
        var task = Solve.api("/tasks?waitMs=0", input, deadline);
        while (task.get("status").getAsString().matches("pending|solving")) {
            String id = URLEncoder.encode(task.get("id").getAsString(), StandardCharsets.UTF_8);
            task = Solve.api("/tasks/" + id + "?waitMs=15000", null, deadline);
            if (task.get("status").getAsString().matches("pending|solving")) Thread.sleep(1000);
        }
        if (!task.get("status").getAsString().equals("ready")) throw new IllegalStateException("Task failed");
        var solution = task.getAsJsonObject("solution");
        var options = new ChromeOptions();
        options.addArguments("--proxy-server=" + proxy, "--user-agent=" + solution.get("userAgent").getAsString());
        var browser = new ChromeDriver(options);
        try {
            browser.executeCdpCommand("Network.enable", Map.of());
            browser.executeCdpCommand("Network.setExtraHTTPHeaders", Map.of("headers",
                Map.of("sec-ch-ua", solution.get("secChUa").getAsString())));
            browser.get(target);
            var wait = new WebDriverWait(browser, Duration.ofSeconds(20));
            var field = wait.until(driver -> driver.findElement(By.name("g-recaptcha-response")));
            browser.executeScript("arguments[0].value=arguments[1]; arguments[0].dispatchEvent(new Event('input',{bubbles:true}));",
                field, solution.get("token").getAsString());
            browser.findElement(By.cssSelector("button[type=submit]")).click();
            wait.until(driver -> driver.findElement(By.id("success"))); // Your application's success marker.
        } finally { browser.quit(); }
    }
}

The order matters: ChromeOptions sets the proxy and returned user agent, then ChromeDriver sets the returned sec-ch-ua header through CDP before browser.get(target). Don't navigate first with an unrelated identity and change it only when submitting the token.

This example deliberately submits a single proxy, so Chrome can use that same proxy without having to select among candidates. Keep the target URL consistent with the hostname and action used for the task.

Adapt the form before running BrowserSolve

The browser example targets an owned form with a CAPTCHA response field, a submit button, and a success element. Change these application-specific values before running BrowserSolve.main with the Maven dependencies on its classpath.

  • action: replace login with the action your application uses for this submission.
  • g-recaptcha-response: confirm the field name your form actually submits. The example assigns solution.token and dispatches a bubbling input event.
  • button[type=submit]: replace this selector if it doesn't identify the intended form's submit button.
  • success: replace this element ID with a state that appears only after the current submission succeeds.

Setting a field and dispatching an event isn't a universal CAPTCHA callback. If your application's form logic doesn't consume that field or event, adapt the submission step to its actual binding. Inspect the outgoing request to confirm it contains solution.token, not the task ID or an old token.

The final WebDriver wait checks for an application's success element. Choose that marker carefully: an element already present before submission proves nothing about the new request. A ready task means solvethree generated a solution; target acceptance is established by the application's response.

Handle failures without creating another task unnecessarily

The API returns a non-success HTTP response.

Check the HTTP response separately from the task status. Solve.api throws before treating it as a success payload.

Try this: Diagnose the request error before retrying. Confirm the request fields and active site profile rather than repeatedly sending unchanged invalid input.

The task reaches failed.

Read errorCode and error from the task response. BrowserSolve's generic exception doesn't display them, although the Solve.java standalone example does.

Try this: Stop polling that task and expose those fields in your error handling. Failed solves are refunded; don't assume every failure is a credit problem.

Creation times out or the local deadline expires.

Determine whether you already have a task ID or saved the original request body and requestId. A connection timeout doesn't establish that creation failed.

Try this: Resume retrieval using the task ID, or recover creation with the same requestId and identical inputs. Save the body before creation if recovery must survive a restart; rerunning the supplied main method generates a new requestId.

The task is ready, but the application rejects the submission.

Inspect the outgoing token, action, target URL, proxy, user agent, and sec-ch-ua. Confirm the application actually submitted the newly assigned field value.

Try this: Correct the mismatched value or submission binding before paying for another solve. Keep the successful proxy and returned identity together.

The 180-second API deadline and 20-second application wait are local limits chosen by the examples, not service guarantees. For additional API error details, see CAPTCHA API errors.

Run your first task.

Start with $5 in workspace credit.

Create a workspace