Scarnox · Blog

Captcha solver in Java with HttpClient: working example (2026)

A captcha solver in Java is one HTTP call. Java 11 and later ship java.net.http.HttpClient, so the only dependency this guide needs is Gson, for JSON. You post the sitekey and page URL to the Scarnox API, read solution.token, and submit the token to the site's form. This guide covers PopularCaptcha, which Scarnox solves today on a proxyless basis.

Start with free trial credit, no card needed and try the code on a page you control before using it on anything else.

Requirements

The solver class

The class wraps one HttpClient that keeps cookies, so a session can carry through the form submit. The solve method sets the Bearer header, sends the JSON body, and checks errorId:

// ScarnoxSolver.java
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

public class ScarnoxSolver {
    private static final String API_URL = "https://api.scarnox.com/api/tasks/create";
    private static final Gson GSON = new Gson();

    private final String apiKey;
    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
            .build();

    public ScarnoxSolver(String apiKey) {
        this.apiKey = apiKey;
    }

    /** Returns the token. The call blocks until the token is ready. */
    public String solve(String sitekey, String pageUrl, boolean invisible) throws Exception {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("captcha_type", "PopularCaptchaTaskProxyless");
        body.put("site_key", sitekey);
        body.put("site_url", pageUrl);
        if (invisible) {
            body.put("is_invisible", true);
        }

        HttpRequest request = HttpRequest.newBuilder(URI.create(API_URL))
                .timeout(Duration.ofSeconds(185))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .build();

        HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

        JsonObject data;
        try {
            data = JsonParser.parseString(response.body()).getAsJsonObject();
        } catch (RuntimeException e) {
            throw new IllegalStateException("HTTP " + response.statusCode() + ": not JSON");
        }

        if (data.get("errorId").getAsInt() != 0) {
            throw new IllegalStateException(
                    data.get("errorCode").getAsString() + ": "
                    + data.get("errorDescription").getAsString());
        }

        return data.getAsJsonObject("solution").get("token").getAsString();
    }

    /** Posts the token to the site's form and returns the HTTP status. */
    public int submit(String pageUrl, String email, String token) throws Exception {
        // Load the page first so the session cookie is set.
        http.send(HttpRequest.newBuilder(URI.create(pageUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding());

        String form = "email=" + enc(email)
                + "&h-captcha-response=" + enc(token)
                + "&g-recaptcha-response=" + enc(token);

        HttpRequest request = HttpRequest.newBuilder(URI.create(pageUrl))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(form))
                .build();

        return http.send(request, HttpResponse.BodyHandlers.discarding()).statusCode();
    }

    private static String enc(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}

The Authorization header uses the Bearer prefix. The request timeout is 185 seconds, because the API blocks until the token is ready. There is no polling step.

The main program

The main method solves with a few retries, then submits the token:

// Main.java
public class Main {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("SCARNOX_API_KEY");
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalStateException("Set SCARNOX_API_KEY");
        }

        String pageUrl = "https://example.com/signup";
        String sitekey = "YOUR-SITEKEY-UUID";
        ScarnoxSolver solver = new ScarnoxSolver(apiKey);

        String token = null;
        for (int attempt = 1; attempt <= 3 && token == null; attempt++) {
            try {
                token = solver.solve(sitekey, pageUrl, false);
            } catch (IllegalStateException e) {
                System.err.println("Attempt " + attempt + " failed: " + e.getMessage());
                Thread.sleep(attempt * 1000L);
            }
        }
        if (token == null) {
            throw new IllegalStateException("No token after 3 attempts");
        }

        int status = solver.submit(pageUrl, "you@example.com", token);
        System.out.println("Status: " + status);
    }
}

Run it:

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
javac -cp gson-2.10.1.jar *.java
java -cp .:gson-2.10.1.jar Main

Why it is structured this way

One client per solver. HttpClient is thread-safe and meant to be reused. Creating one per request wastes connections.

Cookies on. The CookieManager on the client keeps the session that loaded the form, so the token is sent with the cookie the site expects.

Explicit error check. The API returns errorId as 0 on success. Any other value includes errorCode and errorDescription, and the solver throws with that text. The token is at solution.token.

Both captcha fields. The form gets h-captcha-response and g-recaptcha-response. Most sites read one or the other.

Timing

The token expires in about two minutes. Solve right before the submit, not at the start of a long job. Captcha token expiry explains how to size the window and tell an expired token from a rejected one.

Invisible widgets

Pass true as the third argument to solve when the widget is invisible. The request then includes is_invisible: true. If the page needs a JavaScript callback with the token, a server-side client cannot trigger it, and a browser automation tool is the better choice. The invisible captcha guide lists the patterns.

Error handling

Retries cover transient failures. Stop retrying on errors that will not change, such as a wrong sitekey or a page URL the widget does not run on, and log them for review. The full policy, including which codes to stop on, is in captcha solver error handling.

Scraping responsibly

If the program scrapes public pages, check robots.txt and the site's terms first. Add a delay between requests and keep only the personal data you need. For the legal background, see is web scraping legal.

Pricing

You pay per successful solve, and failed solves are free. See current pricing for the rates. To begin, start with free trial credit, no card needed.

Next steps