Scarnox · Blog

Resolver captchas en Java con HttpClient: ejemplo funcional (2026)

Un solver de captchas en Java es una sola llamada HTTP. Java 11 y posteriores incluyen java.net.http.HttpClient, así que la única dependencia que necesita esta guía es Gson, para el JSON. Envías la sitekey y la URL de la página a la API de Scarnox, lees solution.token y envías el token al formulario del sitio. Esta guía cubre PopularCaptcha, que Scarnox resuelve hoy sin proxy.

Empieza con crédito de prueba gratis, sin tarjeta y prueba el código en una página que controles antes de usarlo en cualquier otro sitio.

Requisitos

La clase del solver

La clase envuelve un HttpClient que guarda cookies, de modo que una sesión pueda mantenerse hasta el envío del formulario. El método de resolución establece la cabecera Bearer, envía el cuerpo JSON y comprueba 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);
    }
}

La cabecera Authorization usa el prefijo Bearer. El timeout de la petición es de 185 segundos, porque la API se bloquea hasta que el token está listo. No hay ningún paso de sondeo.

El programa principal

El método main resuelve con unos pocos reintentos y luego envía el 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);
    }
}

Ejecútalo:

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

Por qué está estructurado así

Un cliente por solver. HttpClient es seguro para hilos y está pensado para reutilizarse. Crear uno por petición desperdicia conexiones.

Cookies activadas. El CookieManager del cliente mantiene la sesión que cargó el formulario, de modo que el token se envía con la cookie que espera el sitio.

Comprobación explícita de errores. La API devuelve errorId como 0 en caso de éxito. Cualquier otro valor incluye errorCode y errorDescription, y el solver lanza una excepción con ese texto. El token está en solution.token.

Ambos campos de captcha. El formulario recibe h-captcha-response y g-recaptcha-response. La mayoría de los sitios leen uno u otro.

Tiempos

El token caduca en unos dos minutos. Resuelve justo antes del envío, no al principio de un trabajo largo. Caducidad del token de captcha explica cómo dimensionar la ventana y distinguir un token caducado de uno rechazado.

Widgets invisibles

Pasa true como tercer argumento de solve cuando el widget sea invisible. La petición incluirá entonces is_invisible: true. Si la página necesita un callback de JavaScript con el token, un cliente del lado del servidor no puede activarlo, y una herramienta de automatización de navegador es la mejor opción. La guía sobre captchas invisibles recoge los patrones.

Gestión de errores

Los reintentos cubren los fallos temporales. Deja de reintentar en errores que no van a cambiar, como una sitekey incorrecta o una URL de página en la que el widget no se ejecuta, y regístralos para revisarlos. La política completa, incluidos los códigos en los que conviene parar, está en gestión de errores del solver de captchas.

Scrapea de forma responsable

Si el programa hace scraping de páginas públicas, revisa primero el robots.txt y los términos del sitio. Añade una pausa entre peticiones y conserva solo los datos personales que necesites. Para el contexto legal, consulta ¿es legal el web scraping?.

Precios

Pagas por cada resolución correcta, y las resoluciones fallidas son gratuitas. Consulta los precios actuales para ver las tarifas. Para empezar, empieza con crédito de prueba gratis, sin tarjeta.

Próximos pasos