Use the CAPTCHA API with Selenium in Java
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.
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.
Save both Java files
Save the first example as
src/main/java/Solve.javaand the second assrc/main/java/BrowserSolve.java. Keep them in the same package; the supplied files have no package declaration.BrowserSolvecallsSolve.api, notSolve.main. - 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.
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.
| Variable | Value |
|---|---|
SOLVETHREE_API_KEY | Your workspace API key. |
SOLVETHREE_PROXY | An IP-allowlisted HTTP proxy URL without embedded username or password, path, query, or fragment. |
TARGET_URL | The 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.
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.
// 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: replaceloginwith the action your application uses for this submission.g-recaptcha-response: confirm the field name your form actually submits. The example assignssolution.tokenand dispatches a bubblinginputevent.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.
Sources and API reference
Run your first task.
Start with $5 in workspace credit.