Go's strong typing, built-in concurrency, and single-binary deployment make it a solid choice for automation systems. This guide builds a CaptchaAI client library that follows Go conventions — context.Context support, custom http.Client injection, and typed error values.
Package Structure
captchaai/
├── client.go # Main client and solve logic
├── errors.go # Error types
├── types.go # Request/response structs
└── client_test.go # Tests
Error Types
// errors.go
package captchaai
import "fmt"
// APIError represents a CaptchaAI API error response.
type APIError struct {
Code string
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("captchaai: %s (%s)", e.Message, e.Code)
}
// IsFatal returns true if this error should not be retried.
func (e *APIError) IsFatal() bool {
switch e.Code {
case "ERROR_WRONG_USER_KEY", "ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE", "ERROR_IP_NOT_ALLOWED":
return true
}
return false
}
// TimeoutError indicates the solve exceeded the configured timeout.
type TimeoutError struct {
TaskID string
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("captchaai: task %s timed out", e.TaskID)
}
Types
// types.go
package captchaai
import "time"
// ClientOption configures the CaptchaAI client.
type ClientOption func(*Client)
// WithPollInterval sets the polling interval between result checks.
func WithPollInterval(d time.Duration) ClientOption {
return func(c *Client) { c.pollInterval = d }
}
// WithTimeout sets the maximum time to wait for a solution.
func WithTimeout(d time.Duration) ClientOption {
return func(c *Client) { c.timeout = d }
}
// RecaptchaV2Params holds parameters for reCAPTCHA v2 solving.
type RecaptchaV2Params struct {
SiteKey string
PageURL string
Invisible bool
Cookies string
}
// RecaptchaV3Params holds parameters for reCAPTCHA v3 solving.
type RecaptchaV3Params struct {
SiteKey string
PageURL string
Action string
MinScore float64
}
// TurnstileParams holds parameters for Cloudflare Turnstile solving.
type TurnstileParams struct {
SiteKey string
PageURL string
Action string
CData string
}
// ImageParams holds parameters for image/OCR CAPTCHA solving.
type ImageParams struct {
Base64Image string
CaseSensitive bool
MinLength int
MaxLength int
}
type submitResponse struct {
Status int `json:"status"`
Request string `json:"request"`
}
type pollResponse struct {
Status int `json:"status"`
Request string `json:"request"`
}
Client Implementation
// client.go
package captchaai
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
const (
submitURL = "https://ocr.captchaai.com/in.php"
resultURL = "https://ocr.captchaai.com/res.php"
defaultPollInterval = 5 * time.Second
defaultTimeout = 180 * time.Second
)
// Client interacts with the CaptchaAI API.
type Client struct {
apiKey string
httpClient *http.Client
pollInterval time.Duration
timeout time.Duration
}
// New creates a CaptchaAI client with the given API key and options.
func New(apiKey string, opts ...ClientOption) *Client {
c := &Client{
apiKey: apiKey,
httpClient: http.DefaultClient,
pollInterval: defaultPollInterval,
timeout: defaultTimeout,
}
for _, opt := range opts {
opt(c)
}
return c
}
// WithHTTPClient sets a custom HTTP client (e.g., for proxy support).
func WithHTTPClient(hc *http.Client) ClientOption {
return func(c *Client) { c.httpClient = hc }
}
func (c *Client) submit(ctx context.Context, params url.Values) (string, error) {
params.Set("key", c.apiKey)
params.Set("json", "1")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, submitURL, nil)
if err != nil {
return "", fmt.Errorf("captchaai: build request: %w", err)
}
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("captchaai: submit: %w", err)
}
defer resp.Body.Close()
var result submitResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("captchaai: decode submit response: %w", err)
}
if result.Status != 1 {
return "", &APIError{Code: result.Request, Message: "submit failed"}
}
return result.Request, nil
}
func (c *Client) poll(ctx context.Context, taskID string) (string, error) {
deadline := time.After(c.timeout)
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-deadline:
return "", &TimeoutError{TaskID: taskID}
case <-time.After(c.pollInterval):
}
params := url.Values{
"key": {c.apiKey},
"action": {"get"},
"id": {taskID},
"json": {"1"},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resultURL+"?"+params.Encode(), nil)
if err != nil {
return "", fmt.Errorf("captchaai: build poll request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
continue // Retry on network error
}
var result pollResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
continue
}
resp.Body.Close()
if result.Request == "CAPCHA_NOT_READY" {
continue
}
if result.Status == 1 {
return result.Request, nil
}
return "", &APIError{Code: result.Request, Message: "solve failed"}
}
}
// SolveRecaptchaV2 solves a reCAPTCHA v2 challenge.
func (c *Client) SolveRecaptchaV2(ctx context.Context, p RecaptchaV2Params) (string, error) {
params := url.Values{
"method": {"userrecaptcha"},
"googlekey": {p.SiteKey},
"pageurl": {p.PageURL},
}
if p.Invisible {
params.Set("invisible", "1")
}
if p.Cookies != "" {
params.Set("cookies", p.Cookies)
}
taskID, err := c.submit(ctx, params)
if err != nil {
return "", err
}
return c.poll(ctx, taskID)
}
// SolveRecaptchaV3 solves a reCAPTCHA v3 challenge.
func (c *Client) SolveRecaptchaV3(ctx context.Context, p RecaptchaV3Params) (string, error) {
params := url.Values{
"method": {"userrecaptcha"},
"version": {"v3"},
"googlekey": {p.SiteKey},
"pageurl": {p.PageURL},
}
if p.Action != "" {
params.Set("action", p.Action)
}
if p.MinScore > 0 {
params.Set("min_score", strconv.FormatFloat(p.MinScore, 'f', 1, 64))
}
taskID, err := c.submit(ctx, params)
if err != nil {
return "", err
}
return c.poll(ctx, taskID)
}
// SolveTurnstile solves a Cloudflare Turnstile challenge.
func (c *Client) SolveTurnstile(ctx context.Context, p TurnstileParams) (string, error) {
params := url.Values{
"method": {"turnstile"},
"sitekey": {p.SiteKey},
"pageurl": {p.PageURL},
}
if p.Action != "" {
params.Set("action", p.Action)
}
if p.CData != "" {
params.Set("data", p.CData)
}
taskID, err := c.submit(ctx, params)
if err != nil {
return "", err
}
return c.poll(ctx, taskID)
}
// SolveImage solves an image/text CAPTCHA from base64.
func (c *Client) SolveImage(ctx context.Context, p ImageParams) (string, error) {
params := url.Values{
"method": {"base64"},
"body": {p.Base64Image},
}
if p.CaseSensitive {
params.Set("regsense", "1")
}
if p.MinLength > 0 {
params.Set("min_len", strconv.Itoa(p.MinLength))
}
if p.MaxLength > 0 {
params.Set("max_len", strconv.Itoa(p.MaxLength))
}
taskID, err := c.submit(ctx, params)
if err != nil {
return "", err
}
return c.poll(ctx, taskID)
}
// GetBalance returns the current account balance.
func (c *Client) GetBalance(ctx context.Context) (float64, error) {
params := url.Values{
"key": {c.apiKey},
"action": {"getbalance"},
"json": {"1"},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resultURL+"?"+params.Encode(), nil)
if err != nil {
return 0, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var result pollResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, err
}
return strconv.ParseFloat(result.Request, 64)
}
Usage
package main
import (
"context"
"fmt"
"log"
"time"
"your-module/captchaai"
)
func main() {
client := captchaai.New("YOUR_API_KEY",
captchaai.WithTimeout(120*time.Second),
captchaai.WithPollInterval(5*time.Second),
)
ctx := context.Background()
// Check balance
balance, err := client.GetBalance(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Balance: $%.2f\n", balance)
// Solve reCAPTCHA v2
token, err := client.SolveRecaptchaV2(ctx, captchaai.RecaptchaV2Params{
SiteKey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
PageURL: "https://example.com/login",
})
if err != nil {
var apiErr *captchaai.APIError
if errors.As(err, &apiErr) && apiErr.IsFatal() {
log.Fatalf("Fatal API error: %s", apiErr.Code)
}
log.Fatal(err)
}
fmt.Printf("Token: %s...\n", token[:40])
// Solve with context timeout
solveCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
turnstileToken, err := client.SolveTurnstile(solveCtx, captchaai.TurnstileParams{
SiteKey: "0x4AAAAAAADnPIDROrmt1Wwj",
PageURL: "https://example.com/checkout",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Turnstile: %s...\n", turnstileToken[:40])
}
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
context deadline exceeded |
Solve took longer than context timeout | Use longer context timeout or increase client WithTimeout |
captchaai: submit failed (ERROR_ZERO_BALANCE) |
No funds | Top up at CaptchaAI dashboard |
| Polls never complete | Network issues or wrong API URL | Check connectivity; verify URL constants |
Compiler error on errors.As |
Missing import | Add "errors" to imports |
| Custom HTTP client not used | Forgot WithHTTPClient option |
Pass option in New(): captchaai.New(key, captchaai.WithHTTPClient(myClient)) |
FAQ
Why use context.Context instead of a simple timeout?
Context integrates with Go's standard cancellation pattern. If the parent HTTP handler or goroutine is cancelled, the CAPTCHA solve stops immediately — no orphaned polling loops consuming API credits.
How do I use this with a proxy?
Inject a custom http.Client with a proxy transport. This routes all SDK traffic through your proxy without modifying the library.
Should I use go install or vendoring?
For private projects, use go mod vendor. For reusable libraries, publish as a Go module with semantic versioning and let consumers import with go get.
Related Articles
- Captchaai Ip Whitelisting Api Key Security
- Captchaai Api Key Rotation
- Captchaai Api Endpoint Mapping Competitors
Next Steps
Build your Go CAPTCHA client — get your CaptchaAI API key and start with the package above.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.