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:

Go · net/http, complete
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:

Go · create then poll
// 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:

Go · errgroup fan-out
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:

Go · enterprise rqdata
// 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.

Go · submit the form
// 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 rqdata sitekeys. 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?
Not yet. The official clients are npm (TypeScript) and PyPI (Python). For Go we publish the REST surface instead: two endpoints, Bearer auth, JSON in and out. The ~80-line 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?
At least one credit per solve. Billing starts at one credit per hCaptcha challenge round (hCaptcha decides how many: often one, sometimes two or three), charged only on success, so failed, expired, and cancelled solves are never charged. Credits run $0.25 to $0.50 per 1,000 depending on pack size, signup includes 100 free credits, credits never expire, and there is no subscription. See pricing.
How do I solve enterprise sitekeys with rqdata from Go?
Set 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?
Five in flight by default, up to 100 on paid packs. Goroutines make it easy to exceed that by accident, so bound the fan-out with 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?
No. NoneCap solves the challenge on its own servers and returns the token as a string, so plain 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.

Start solving hCaptcha in minutes.

100 free credits on signup. Pay per solve, credits never expire, failed solves never charged.