Eine eigene CaptchaAI-Anbindung in Go besteht aus vier Dateien und zwei HTTP-Aufrufen: Ein POST an in.php übermittelt die Aufgabe, wiederholte GET-Anfragen an res.php holen das Token ab. Alles Weitere – context.Context, typisierte Fehler, konfigurierbare Timeouts – ist Handwerk, und genau das zeigt dieser Leitfaden Datei für Datei. Am Ende steht ein Paket ohne Fremdabhängigkeiten, das in Scraper, Integrationstests und Cron-Jobs passt.
Wer den ersten API-Aufruf noch vor sich hat, beginnt beim Schnellstart für den API-Schlüssel; hier geht es um die Bibliothek darum herum.
Anforderungen an den Client festlegen
Signaturen ändert man später ungern. Deshalb zuerst die fünf Entscheidungen, die alles Weitere bestimmen:
- Abbrechbar: Jede Solve-Methode nimmt ein
context.Contextentgegen. Bricht der übergeordnete HTTP-Handler ab, endet auch das Polling. - Austauschbarer HTTP-Stack: Ein eigener
*http.Clienterlaubt Proxys, Transport-Timeouts und Instrumentierung, ohne die Bibliothek anzufassen. - Unterscheidbare Fehler: Ein leeres Guthaben ist ein anderer Fall als eine Zeitüberschreitung; der Aufrufer muss beides per
errors.Astrennen können. - Nebenläufig nutzbar: ein Client-Objekt, viele Goroutinen, kein gemeinsamer veränderlicher Zustand.
- Ohne Fremdabhängigkeiten:
net/http,net/urlundencoding/jsongenügen.
Abgedeckt werden reCAPTCHA v2 samt Invisible-Variante, reCAPTCHA v3, Cloudflare Turnstile und Bild-CAPTCHAs per OCR. hCaptcha und FunCaptcha stehen bei CaptchaAI nicht zur Verfügung, GeeTest v4 ist nur als „bald verfügbar“ angekündigt – diese Methoden gehören nicht in Ihr Paket.
Paketstruktur
Vier Dateien mit klaren Zuständigkeiten sind in Go üblicher als eine einzelne große captchaai.go:
captchaai/
├── client.go # Main client and solve logic
├── errors.go # Error types
├── types.go # Request/response structs
└── client_test.go # Tests
client.go bleibt die einzige Datei, die HTTP kennt – das hält die übrigen frei von Netzwerklogik.
Fehlertypen vor der Logik definieren
Go-Bibliotheken werden an ihrer Fehlerbehandlung gemessen. Zwei Typen genügen: APIError für alles, was die API meldet, und TimeoutError für ausbleibende Lösungen.
// 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)
}
Entscheidend ist IsFatal: Ein falscher API-Schlüssel oder ein aufgebrauchtes Guthaben wird durch einen erneuten Versuch nicht besser, solche Codes gehören sofort nach oben. Netzwerkfehler und CAPCHA_NOT_READY sind dagegen normale Zwischenzustände – die vollständige Liste steht in der Referenz zu den Antwortformaten und Fehlercodes.
Parameter-Typen und funktionale Optionen
Pro CAPTCHA-Typ ein Parameter-Struct: Das hält die Aufrufe lesbar und erlaubt spätere Felder, ohne Signaturen zu brechen. Die Client-Konfiguration läuft über funktionale Optionen (ClientOption) – das idiomatische Go-Muster dafür.
// 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
}
// 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"`
}
Beachten Sie die unterschiedlichen Feldnamen: reCAPTCHA erwartet googlekey, Turnstile sitekey. Solche Details verschwinden hinter typisierten Structs, statt sich als rohe url.Values durch den Anwendungscode zu ziehen.
Übermitteln und Status abfragen
Das Herzstück sind zwei unexportierte Methoden: submit schickt die Aufgabe an in.php und liefert die Task-ID, poll fragt res.php in festen Intervallen ab, bis ein Token oder ein endgültiger Fehler vorliegt.
// 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)
}
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)
}
Drei Details lohnen einen zweiten Blick:
- Das
selectbehandelt drei Fälle gleichrangig: abgebrochener Kontext, abgelaufenes Timeout, nächster Abfragezeitpunkt. Keintime.Sleep, keine blockierte Goroutine. - Ein Netzwerkfehler beim Polling führt zu
continue, nicht zum Abbruch – die Aufgabe läuft serverseitig weiter. CAPCHA_NOT_READYist kein Fehler, sondern die Aufforderung, weiter abzufragen.
Als Startwerte haben sich 5 s Abfrageintervall und 120–180 s Gesamtfrist bewährt; engere Budgets setzen Sie pro Aufruf über context.WithTimeout.
Der Client im Einsatz
Ein Aufruf besteht aus drei Schritten: Client erzeugen, Kontext bereitstellen, Solve-Methode aufrufen. Das folgende Programm prüft das Guthaben und löst dann reCAPTCHA v2 und Turnstile.
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])
}
Zwei Hinweise: errors gehört in die Import-Liste, sobald errors.As verwendet wird. Und das gelöste Token tragen Sie unmittelbar in das Formularfeld g-recaptcha-response beziehungsweise cf-turnstile-response ein – Tokens sind kurzlebig.
Threads im Tarif, Goroutinen im Code
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung: Ein Thread ist ein CAPTCHA in Bearbeitung, danach nimmt derselbe Thread das nächste. Daraus folgt eine einfache Regel für die Bibliothek – die Zahl der Goroutinen, die gleichzeitig eine Solve…-Methode aufrufen, sollte das Thread-Kontingent des Tarifs nicht überschreiten.
| Tarif | Preis pro Monat | Threads | Sinnvolle Parallelität im Code |
|---|---|---|---|
| BASIC | 15 $ | 5 | kleine Test-Suite, ein Worker |
| STANDARD | 30 $ | 15 | CI-Job mit parallelen Szenarien |
| ADVANCE | 90 $ | 50 | Dauerbetrieb mit Warteschlange |
| PREMIUM | 170 $ | 100 | mehrere Worker-Instanzen |
Preise in US-Dollar; jeder Tarif enthält unbegrenzte Lösungen pro Thread, also keine Zusatzkosten pro CAPTCHA. Im Code bildet ein gepufferter Kanal als Semaphore genau dieses Kontingent ab; mit golang.org/x/sync/errgroup erreicht SetLimit dasselbe.
Zeitbudget, Wiederholungen und Guthaben
Drei Regeln haben sich im Dauerbetrieb bewährt:
- Timeout getrennt vom Fehlercode protokollieren. Ein
TimeoutErrorsagt nur, dass die Antwort ausblieb – nicht, dass die Aufgabe fehlerhaft war. - Nur bei nicht-fatalen Codes erneut versuchen.
IsFatalliefert die Entscheidung; für alles andere genügt exponentielles Backoff mit zwei bis drei Versuchen. - Guthaben überwachen.
GetBalancestündlich aus einem Hintergrund-Job abzufragen und unterhalb eines Schwellwerts zu alarmieren, verhindert den unangenehmsten Fehlerfall: eine Pipeline, die still gegenERROR_ZERO_BALANCEläuft.
Fehlerbehebung
| Symptom | Ursache | Vorgehen |
|---|---|---|
context deadline exceeded |
Kontextfrist kürzer als die Lösungszeit | Frist erhöhen oder WithTimeout anpassen |
| Token kommt an, die Zielseite lehnt es ab | sitekey, pageurl oder Sitzungskontext passen nicht zusammen |
Parameter erneut auslesen, Token in derselben Sitzung verwenden |
| Polling läuft regelmäßig ins Timeout | Gesamtfrist zu knapp oder Fehlercodes werden verschluckt | Alle 5–10 s abfragen, Timeout und Fehlercode getrennt protokollieren |
Compilerfehler bei errors.As |
fehlender Import | "errors" ergänzen |
| Eigener HTTP-Client wird ignoriert | Option nicht übergeben | captchaai.New(key, captchaai.WithHTTPClient(myClient)) |
Betrieb im DACH-Umfeld
Der Deployment-Vorteil von Go zeigt sich im Betrieb: eine einzelne, statisch gelinkte Binärdatei. Auf einem kleinen Server bei Hetzner, netcup oder IONOS läuft der Worker ohne installierte Runtime, und in GitLab CI – in vielen deutschen Unternehmen Standard neben GitHub Actions – genügt ein schlanker Container mit CGO_ENABLED=0.
Zwei Punkte für die Praxis: Der API-Schlüssel gehört als maskierte CI-Variable in die Pipeline, nicht ins Repository. Und für Scraping-Workloads klären Sie den datenschutzrechtlichen Rahmen vorab – IP-Adressen gelten nach DSGVO als personenbezogene Daten, die Rechtsgrundlage liegt beim Betreiber.
Häufige Fragen
Welche CAPTCHA-Typen sollte die Bibliothek abbilden?
Die regulär angebotenen Typen: reCAPTCHA v2, v2 Invisible, v2 Callback, v2 und v3 Enterprise, reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, Bild- und Rasterbild-CAPTCHAs sowie BLS. CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta-Phase. Weitere Typen ergänzen Sie nach demselben Muster – neues Params-Struct, neue Solve…-Methode, dieselbe submit/poll-Basis.
Wie viele Goroutinen darf ich gleichzeitig starten?
Höchstens so viele, wie Ihr Tarif Threads enthält – bei STANDARD (30 $) also 15 gleichzeitige Aufrufe. Mehr Goroutinen erhöhen nur die Wartezeit, nicht den Durchsatz. Begrenzen Sie die Parallelität im Code, statt auf serverseitige Drosselung zu setzen.
Warum liefert das Polling CAPCHA_NOT_READY?
Weil die Aufgabe noch bearbeitet wird – bei den ersten Abfragen der Normalfall. Fragen Sie im Abstand von 5–10 s weiter ab und behandeln Sie den Wert getrennt von echten Fehlercodes; die Schreibweise ohne „T“ ist so vorgesehen.
Wie teste ich den Client ohne echte API-Aufrufe?
Mit httptest.NewServer und einer festen JSON-Antwort. Injizieren Sie per WithHTTPClient einen *http.Client mit eigenem http.RoundTripper, der die Aufrufe umleitet – oder machen Sie submitURL und resultURL zu Paketvariablen, die der Test überschreibt. Beides hält die Tests offline.