Scarnox · Blog

Resolver captchas en Go: ejemplo funcional con net/http (2026)

Un solver de captchas en Go no necesita paquetes de terceros. net/http y encoding/json de la biblioteca estándar cubren todo el flujo: un POST a la API de Scarnox, una respuesta JSON y un envío del formulario al sitio. Esta guía construye ese flujo para PopularCaptcha, que Scarnox resuelve hoy sin proxy.

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

Requisitos

El programa completo

Este único archivo resuelve el captcha, reintenta los fallos temporales y envía el token al formulario. Guárdalo como main.go:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"time"
)

const apiURL = "https://api.scarnox.com/api/tasks/create"

// The solve call blocks until the token is ready, so the timeout is long.
var apiClient = &http.Client{Timeout: 185 * time.Second}

type createRequest struct {
	CaptchaType string `json:"captcha_type"`
	SiteKey     string `json:"site_key"`
	SiteURL     string `json:"site_url"`
	IsInvisible bool   `json:"is_invisible,omitempty"`
}

type createResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Solution         struct {
		Token string `json:"token"`
	} `json:"solution"`
}

func solveCaptcha(apiKey, sitekey, pageURL string, invisible bool) (string, error) {
	body, err := json.Marshal(createRequest{
		CaptchaType: "PopularCaptchaTaskProxyless",
		SiteKey:     sitekey,
		SiteURL:     pageURL,
		IsInvisible: invisible,
	})
	if err != nil {
		return "", err
	}

	req, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := apiClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	var out createResponse
	if err := json.Unmarshal(raw, &out); err != nil {
		return "", fmt.Errorf("HTTP %d, response was not JSON: %s", resp.StatusCode, raw)
	}
	if out.ErrorID != 0 {
		return "", fmt.Errorf("%s: %s", out.ErrorCode, out.ErrorDescription)
	}
	return out.Solution.Token, nil
}

// solveWithRetry retries transient failures. Failed solves are not charged.
func solveWithRetry(apiKey, sitekey, pageURL string, attempts int) (string, error) {
	var lastErr error
	for i := 1; i <= attempts; i++ {
		token, err := solveCaptcha(apiKey, sitekey, pageURL, false)
		if err == nil {
			return token, nil
		}
		lastErr = err
		log.Printf("attempt %d failed: %v", i, err)
		time.Sleep(time.Duration(i) * time.Second)
	}
	return "", lastErr
}

func main() {
	apiKey := os.Getenv("SCARNOX_API_KEY")
	if apiKey == "" {
		log.Fatal("set SCARNOX_API_KEY")
	}

	const pageURL = "https://example.com/signup"
	const sitekey = "YOUR-SITEKEY-UUID"

	token, err := solveWithRetry(apiKey, sitekey, pageURL, 3)
	if err != nil {
		log.Fatal(err)
	}

	// A cookie jar keeps the session the site ties the captcha to.
	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Fatal(err)
	}
	site := &http.Client{Jar: jar, Timeout: 60 * time.Second}

	form := url.Values{}
	form.Set("email", "you@example.com")
	form.Set("h-captcha-response", token)
	form.Set("g-recaptcha-response", token)

	resp, err := site.PostForm(pageURL, form)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}

Ejecútalo:

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
go run main.go

Cómo encajan las piezas

La petición. solveCaptcha serializa una estructura en el cuerpo JSON que espera la API y establece la cabecera Authorization. El prefijo Bearer es obligatorio.

El timeout. La API se bloquea hasta que el token está listo, así que el timeout del cliente es de 185 segundos. No lo configures más bajo, o cancelarás resoluciones que estaban a punto de terminar.

La comprobación de errores. La API devuelve errorId como 0 en caso de éxito. Cualquier otro valor llega con errorCode y errorDescription, y la función los devuelve como error. El token está en solution.token.

El envío. El token va en el campo h-captcha-response, y g-recaptcha-response recibe el mismo valor. La mayoría de los sitios leen uno u otro.

Muchas páginas de registro y búsqueda vinculan el captcha a una sesión. El cookiejar del cliente del sitio mantiene esas cookies entre el GET que muestra el formulario y el POST que lo envía. Si lo omites, el sitio puede rechazar un token que por lo demás era válido.

Tiempos

El token caduca en unos dos minutos, así que resuelve justo antes de enviar. No resuelvas al principio de un trabajo largo y luego esperes. Caducidad del token de captcha explica cómo dimensionar la ventana.

Widgets invisibles

Si el widget de la página es invisible, pon el argumento invisible a true. La petición incluirá entonces is_invisible: true. Algunas páginas también necesitan que se llame a una función callback con el token. Busca esa función en el script de la página. La guía sobre captchas invisibles recoge los patrones.

Concurrencia

Go facilita resolver muchos captchas a la vez con goroutines. Mantén el número de resoluciones concurrentes dentro de lo que pueden asumir tu cuenta y el sitio de destino. Un dominio no debería recibir una ráfaga de peticiones, aunque tu solver pueda seguir el ritmo. Respeta el robots.txt y los términos del sitio, y limita el scraping a datos públicos que tengas permiso para recopilar.

Precios

Scarnox cobra por cada resolución correcta. Las resoluciones fallidas son gratuitas. Consulta los precios actuales para ver las tarifas. Para probar la API, empieza con crédito de prueba gratis, sin tarjeta.

Próximos pasos