Skip to content
ZeroCaptcha

Go SDK

The official ZeroCaptcha client for Go: create a Cloudflare Turnstile task or a Cloudflare challenge page’s task, wait for its result, read your balance, and check a task callback’s signature. It uses the standard library only, and needs Go 1.22 or later.

Every task is real and paid from your prepaid balance, and only a task that succeeds is charged.

Terminal window
go get github.com/zerocaptcha/zerocaptcha-go

The module is not published yet. Until it is, call the API with net/http, as the quickstart’s Go program does: it makes the same calls.

Give the client your API key (zc_live_…, from the dashboard’s API keys page) and the API’s address. Keep both in your environment rather than in your code.

package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
zerocaptcha "github.com/zerocaptcha/zerocaptcha-go"
)
func main() {
client, err := zerocaptcha.NewClient(os.Getenv("ZEROCAPTCHA_KEY"), os.Getenv("ZEROCAPTCHA_API"))
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Create a task and wait for its token: one call.
token, err := client.Solve(ctx, zerocaptcha.NewTask{
WebsiteURL: "https://shop.example.com/login", // the page with the widget
WebsiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey
// The widget's data-action and data-cdata, or the action and cData options of
// turnstile.render(). Leave out any the widget does not set.
Action: "login",
CData: "session-7f3a9c2e",
// Proxy: "http://user:pass@proxy.example.net:8080", // to solve through your own proxy
// CallbackURL: "https://hooks.example.com/zerocaptcha", // to be called when it ends
})
var failed *zerocaptcha.TaskFailedError
switch {
case errors.As(err, &failed):
fmt.Println(failed.Code) // such as ERROR_CAPTCHA_UNSOLVABLE; nothing was charged
case err != nil:
log.Fatal(err)
default:
fmt.Println(token)
}
// Or step by step.
task, err := client.CreateTask(ctx, zerocaptcha.NewTask{
WebsiteURL: "https://shop.example.com/login",
WebsiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5",
Action: "login", // the widget's data-action, if it sets one
CData: "session-7f3a9c2e", // the widget's data-cdata, if it sets one
}, zerocaptcha.CreateOptions{
// Your ID for this task, sent as the Idempotency-Key; one is made for you when you give none.
IdempotencyKey: "login-2026-10-01-0001",
})
if err != nil {
log.Fatal(err)
}
done, err := client.WaitForResult(ctx, task.ID, zerocaptcha.WaitOptions{Timeout: 2 * time.Minute})
if err != nil {
log.Fatal(err)
}
fmt.Println(done.Solution.Token, done.Cost)
// Your balance, in US dollars.
balance, err := client.GetBalance(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(balance.Available)
}
Function What it does
NewClient(apiKey, baseURL, options...) A client; WithHTTPClient swaps in your own *http.Client, such as one with a proxy.
CreateTask(ctx, NewTask{...}, CreateOptions{...}) Creates a task: WebsiteURL, WebsiteKey, and optionally Type, Action, CData, Proxy, CallbackURL.
GetTask(ctx, id) Reads a task; its token is in Solution while it is available.
WaitForResult(ctx, id, WaitOptions{...}) Polls every 2 seconds, for up to 3 minutes by default, until the task ends.
Solve(ctx, NewTask{...}) CreateTask then WaitForResult: the token.
CreateChallengeTask(ctx, NewChallengeTask{...}) Creates a Cloudflare challenge page’s task, through your proxy: WebsiteURL, Proxy, optionally CallbackURL.
SolveChallenge(ctx, NewChallengeTask{...}) CreateChallengeTask then WaitForResult: a *Clearance with CfClearance, UserAgent and TokenExpiresAt.
GetBalance(ctx) Available, Held and Currency, as decimal strings.
VerifySignature(secret, header, body, tolerance, now) Whether a callback is genuine; zero values mean five minutes and now.
  • Proxy: "http://user:pass@proxy.example.net:8080" solves a task through your proxy.
  • CreateTask sends an Idempotency-Key with every call, one of its own unless you give one in CreateOptions, so retrying it never makes a second task.
  • A request the API asks you to slow down (429) or cannot serve for a moment (502, 503, 504) is tried again after the wait it asks for, three times in all, as is one that got no answer or an answer cut short, with the same Idempotency-Key. Any other refusal is an *APIError with the API’s Code, such as insufficient_funds, and its RequestID.
  • WaitForResult never runs past its Timeout: each read gets only the time left, and a retry that would wait longer than that is not made. A task that fails or expires is a *TaskFailedError; a wait that runs out is a *WaitTimeoutError, with the task as last read (Task, nil if no read finished in time), and you can wait again. Each call stops when its context does.

A challenge page is passed through your proxy, and gives the cf_clearance cookie with the user agent it is bound to. Send both, through the same proxy:

clearance, err := client.SolveChallenge(ctx, zerocaptcha.NewChallengeTask{
WebsiteURL: "https://shop.example.com/",
Proxy: os.Getenv("PROXY_URL"), // such as http://user:pass@proxy.example.net:8080
})

A task created with CallbackURL is POSTed to it once it ends, with the task as JSON. Check each call’s signature against the raw body, before you parse it:

func callback(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
secret := os.Getenv("ZEROCAPTCHA_CALLBACK_SECRET")
if !zerocaptcha.VerifySignature(secret, r.Header.Get(zerocaptcha.SignatureHeader), body, 0, time.Time{}) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusNoContent)
}

A call older than five minutes does not verify, so a recorded call cannot be replayed. See Polling and callbacks.