Captcha solver in Go: working example with net/http (2026)
A captcha solver in Go needs no third-party packages. The standard library's net/http and encoding/json cover the whole flow: one POST to the Scarnox API, one JSON response, and one form submit to the site. This guide builds that flow for PopularCaptcha, which Scarnox solves today on a proxyless basis.
Start with free trial credit, no card needed and run the program against a page you control before you use it elsewhere.
Requirements
- Go 1.21 or newer.
- A Scarnox API key, read from an environment variable.
- The sitekey of the page's PopularCaptcha widget. If you need help finding it, see how to find a captcha sitekey.
The complete program
This single file solves the captcha, retries transient failures, and submits the token to the form. Save it as 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)
}
Run it:
export SCARNOX_API_KEY=scarnox_YOURAPIKEY
go run main.go
How the pieces fit
The request. solveCaptcha marshals a struct into the JSON body the API expects and sets the Authorization header. The Bearer prefix is required.
The timeout. The API blocks until the token is ready, so the client timeout is 185 seconds. Do not set it lower, or you will cancel solves that were about to finish.
The error check. The API returns errorId as 0 on success. Any other value comes with errorCode and errorDescription, and the function returns them as an error. The token is at solution.token.
The submit. The token goes into the h-captcha-response field, and g-recaptcha-response gets the same value. Most sites read one or the other.
Use a cookie jar
Many sign-up and search pages tie the captcha to a session. The cookiejar on the site client keeps those cookies between the GET that shows the form and the POST that submits it. If you skip it, the site may reject a token that was otherwise valid.
Timing
The token expires in about two minutes, so solve right before you submit. Do not solve at the start of a long job and then wait. Captcha token expiry explains how to size the window.
Invisible widgets
If the page's widget is invisible, set the invisible argument to true. The request then includes is_invisible: true. Some pages also need a callback function called with the token. Check the page's script for it. The invisible captcha guide lists the patterns.
Concurrency
Go makes it easy to solve many captchas at once with goroutines. Keep the number of concurrent solves within what your account and the target site can handle. A single domain should not receive a burst of requests, even if your solver can keep up. Respect the site's robots.txt and terms, and keep scraping to public data you are allowed to collect.
Pricing
Scarnox bills per successful solve. Failed solves are free. See current pricing for the rates. To try the API, start with free trial credit, no card needed.