API-Tutorials

Erstellen einer Go-Client-Bibliothek für die CaptchaAI-API

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.Context entgegen. Bricht der übergeordnete HTTP-Handler ab, endet auch das Polling.
  • Austauschbarer HTTP-Stack: Ein eigener *http.Client erlaubt 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.As trennen können.
  • Nebenläufig nutzbar: ein Client-Objekt, viele Goroutinen, kein gemeinsamer veränderlicher Zustand.
  • Ohne Fremdabhängigkeiten: net/http, net/url und encoding/json genü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:

  1. Das select behandelt drei Fälle gleichrangig: abgebrochener Kontext, abgelaufenes Timeout, nächster Abfragezeitpunkt. Kein time.Sleep, keine blockierte Goroutine.
  2. Ein Netzwerkfehler beim Polling führt zu continue, nicht zum Abbruch – die Aufgabe läuft serverseitig weiter.
  3. CAPCHA_NOT_READY ist 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 TimeoutError sagt nur, dass die Antwort ausblieb – nicht, dass die Aufgabe fehlerhaft war.
  • Nur bei nicht-fatalen Codes erneut versuchen. IsFatal liefert die Entscheidung; für alles andere genügt exponentielles Backoff mit zwei bis drei Versuchen.
  • Guthaben überwachen. GetBalance stündlich aus einem Hintergrund-Job abzufragen und unterhalb eines Schwellwerts zu alarmieren, verhindert den unangenehmsten Fehlerfall: eine Pipeline, die still gegen ERROR_ZERO_BALANCE lä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.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.