Solve hCaptcha in Go.
There's no official Go SDK, and you won't miss it: NoneCap's REST API is two
endpoints, and idiomatic net/http covers both in about 80 lines. POST a
sitekey and page URL, get back a real P1_ token, put it in the form.
Signup comes with 100 free credits, so you can run everything on this page before
paying anything.
Why there's no wrapper to install
The official SDKs are TypeScript on npm and Python on PyPI. Go
isn't on that list yet, and rather than ship a thin module that just shuffles structs
into JSON, this page gives you the structs. The entire API surface a Go program
needs is POST /v1/solves and GET /v1/solves/{id},
Bearer auth, JSON both ways. That's a morning's worth of code in most languages and
about ten minutes in Go.
To be clear about where the work happens: not in your process. You send NoneCap the sitekey and the page URL; it solves the challenge on its own servers and hands back a normal hCaptcha token as a string. You don't need chromedp or rod in the stack unless your target requires a full browser for unrelated reasons.
The whole client in one file
This compiles and runs as-is with NONECAP_KEY in the environment. It's
shaped the way you'd actually ship it: a context with a hard deadline, an error on
any non-2xx response, and a typed response struct instead of
map[string]any:
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
const api = "https://api.nonecap.com/v1"
type solveRequest struct {
Type string `json:"type"` // "hcaptcha" | "hcaptcha_enterprise"
Sitekey string `json:"sitekey"`
URL string `json:"url"`
Rqdata string `json:"rqdata,omitempty"` // enterprise only
}
type solveError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type solve struct {
ID string `json:"id"`
Status string `json:"status"` // pending|solving|solved|failed|expired|cancelled
Token string `json:"token"`
CreditsCharged int `json:"credits_charged"`
Error *solveError `json:"error"`
}
func solveHCaptcha(ctx context.Context, key, sitekey, pageURL string) (string, error) {
body, err := json.Marshal(solveRequest{Type: "hcaptcha", Sitekey: sitekey, URL: pageURL})
if err != nil {
return "", err
}
// ?wait=90 holds the connection until the solve is terminal,
// so the token usually comes back on this first response.
s, err := call(ctx, key, http.MethodPost, "/solves?wait=90", bytes.NewReader(body))
if err != nil {
return "", err
}
// A 202 means it was still in flight after 90s. Re-attach until
// it's terminal; ctx puts a hard ceiling on the whole thing.
for s.Status == "pending" || s.Status == "solving" {
s, err = call(ctx, key, http.MethodGet, "/solves/"+s.ID+"?wait=90", nil)
if err != nil {
return "", err
}
}
if s.Status != "solved" {
if s.Error != nil {
return "", fmt.Errorf("solve %s: %s (%s)", s.Status, s.Error.Message, s.Error.Code)
}
return "", fmt.Errorf("solve %s", s.Status)
}
return s.Token, nil
}
func call(ctx context.Context, key, method, path string, body io.Reader) (solve, error) {
var s solve
req, err := http.NewRequestWithContext(ctx, method, api+path, body)
if err != nil {
return s, err
}
req.Header.Set("Authorization", "Bearer "+key)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return s, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return s, fmt.Errorf("nonecap: %s: %s", resp.Status, b)
}
return s, json.NewDecoder(resp.Body).Decode(&s)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
token, err := solveHCaptcha(ctx, os.Getenv("NONECAP_KEY"),
"f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8", "https://example.com/login")
if err != nil {
log.Fatal(err)
}
fmt.Println(token) // P1_eyJ0eXAi...
}
The interesting part is ?wait=90. It tells the server to hold the
connection open until the solve reaches a terminal state, so the token usually
arrives on the first response and you never write a polling loop. If the solve is
still in flight after 90 seconds the API responds 202 with status
pending or solving, and the loop re-attaches with
GET ?wait=90. The context deadline caps the total wait, and because
every request is built with NewRequestWithContext, cancellation
propagates cleanly: an expired deadline aborts the in-flight HTTP call instead of
leaking it.
A failed or expired status is an error, not a charge.
Credits for unsuccessful solves come back automatically, so the retry costs you
time, not money.
Polling, if you'd rather not hold connections
Long-held requests don't suit every setup. Behind some proxies and serverless
runtimes a 90-second connection is a liability, and a worker draining a queue may
just prefer short requests. Drop ?wait and the POST returns
immediately; then check GET /v1/solves/{id} on a ticker:
// Create without ?wait: the POST returns immediately with
// status "pending". Then poll on a ticker until the solve is
// terminal or the context deadline hits.
func solveByPolling(ctx context.Context, key, sitekey, pageURL string) (string, error) {
body, _ := json.Marshal(solveRequest{Type: "hcaptcha", Sitekey: sitekey, URL: pageURL})
s, err := call(ctx, key, http.MethodPost, "/solves", bytes.NewReader(body))
if err != nil {
return "", err
}
tick := time.NewTicker(2 * time.Second)
defer tick.Stop()
for s.Status == "pending" || s.Status == "solving" {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-tick.C:
}
if s, err = call(ctx, key, http.MethodGet, "/solves/"+s.ID, nil); err != nil {
return "", err
}
}
if s.Status != "solved" {
return "", fmt.Errorf("solve %s", s.Status)
}
return s.Token, nil
}
The select on ctx.Done() matters: it's what turns "poll
forever" into "poll until my deadline". A 2-second interval is a sensible default;
most solves finish in well under a minute. Details are in the
API reference.
Fan-out with goroutines, bounded
Spinning up a goroutine per target is the natural Go move, and it works here, with
one constraint: concurrency is capped per account at 5 solves in flight by default
and up to 100 on paid packs. An unbounded loop over a few hundred targets will blow
past that and collect 429s. errgroup with SetLimit is the
tidiest fix:
import "golang.org/x/sync/errgroup"
// Concurrency is capped per account: 5 solves in flight by
// default, up to 100 on paid packs. SetLimit keeps you under
// the cap; pushing past it gets you 429s, not faster solves.
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(5)
tokens := make([]string, len(targets))
for i, t := range targets {
g.Go(func() error {
tok, err := solveHCaptcha(ctx, key, t.Sitekey, t.URL)
if err != nil {
return err
}
tokens[i] = tok
return nil
})
}
if err := g.Wait(); err != nil {
log.Fatal(err)
} If you'd rather stay in the standard library, a buffered channel as a semaphore does the same job. Either way, size the bound to your account's cap rather than to your CPU count; the work is on NoneCap's side, and your goroutines spend their lives blocked on I/O.
Enterprise sitekeys
Enterprise hCaptcha deployments pass a fresh rqdata blob into
hcaptcha.render() or execute() on the page, and the
challenge is bound to it. The request struct above already carries the field;
switch the type and fill it in:
// Enterprise sitekeys bind each challenge to a fresh rqdata
// blob. Capture it from the live page right before solving;
// it goes stale fast.
body, _ := json.Marshal(solveRequest{
Type: "hcaptcha_enterprise",
Sitekey: sitekey,
URL: pageURL,
Rqdata: rqdata,
})
Capture rqdata from the live page immediately before solving. The
rqdata guide shows where it lives
in the page source and what happens when you send a stale one.
Submitting the token
The token goes into the form POST as the h-captcha-response field, and
in Go that's just url.Values. The part people get wrong is session
consistency: the site's backend verifies the token against the session that was
shown the captcha, so the GET that fetched the page, the solve request's
url, and the final POST should agree, with the same cookie jar and the
same exit IP throughout. Solving under one proxy and submitting under another is the
most common reason a perfectly good token gets rejected.
// Same client (cookie jar) and same exit IP that fetched the
// page. Tokens are checked against the session that saw the
// captcha, so don't solve on one proxy and submit on another.
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar, Timeout: 30 * time.Second}
// 1. GET the page with this client: collects cookies, and you
// read the sitekey out of the HTML here too.
// 2. Solve via NoneCap (above).
// 3. POST the form with the token filled in.
form := url.Values{
"email": {email},
"password": {password},
// hCaptcha's own field name, plus the reCAPTCHA-compat
// alias some backends read instead. Same token in both.
"h-captcha-response": {token},
"g-recaptcha-response": {token},
}
resp, err := client.PostForm("https://target.example/login", form)
Tokens are single-use and valid for roughly two minutes, so solve right before you
submit rather than stockpiling. And read the sitekey from the live page (the
data-sitekey attribute on the widget) instead of hard-coding it; sites
rotate them. What the token contains and how the server side checks it is covered in
the token guide, and the
web scraping guide walks the full
fetch-solve-submit pipeline at scale.
Scope, honestly
NoneCap solves hCaptcha only: regular, invisible, and enterprise
rqdatasitekeys. If your Go program is hitting reCAPTCHA or Cloudflare Turnstile, this API won't help today; both are on the roadmap, not live. Check which captcha you're actually facing before writing the integration; the widgets look alike at a glance.
If part of your stack is TypeScript or Python, those get the official one-call SDKs. For the Go service itself, the code above is the integration: small enough to read in one sitting, and it puts nothing between you and the API.
Last updated June 2026.
Frequently asked
Is there an official Go SDK?
net/http client on this page is the whole integration, and because you own the code there is no wrapper version that can fall behind the API. Request and response shapes live in the API reference.How much does each solve cost?
How do I solve enterprise sitekeys with rqdata from Go?
type to "hcaptcha_enterprise" and include the rqdata field in the JSON body (the struct on this page already has it, tagged omitempty). Capture the blob from the live page right before you solve; enterprise deployments rotate it quickly. The rqdata guide covers where to find it and why stale blobs fail.How many solves can I run in parallel?
errgroup.SetLimit or a buffered-channel semaphore sized to your cap. Requests over the limit get a 429: back off and retry rather than hammering.Do I need a browser in my Go program?
net/http covers the whole flow: fetch the page, solve, POST the form with h-captcha-response set. You only need chromedp or rod if the target site requires a full browser for other reasons, in which case you inject the same token into the page instead. The token guide explains what the string actually is.