API Tutorials

Création d'une bibliothèque client Go pour l'API CaptchaAI

Vous appelez l'API CaptchaAI depuis un service Go et vous recopiez la même boucle d'interrogation dans chaque projet ? Regroupez-la une fois dans un paquet client réutilisable, avec le contexte, un http.Client injectable et des erreurs typées. C'est exactement ce que construit ce guide : un paquet captchaai idiomatique, qui compile en un seul binaire prêt à déployer sur un worker Scaleway ou une fonction OVHcloud, sans dépendance externe.

Le typage fort de Go, sa concurrence native et son déploiement mono-binaire en font un socle solide pour l'automatisation. Nous partons des erreurs, puis des structures de requête et de réponse, avant d'assembler le client autour de context.Context et d'un http.Client injectable.

Pourquoi encapsuler l'API dans un paquet Go

Sans paquet dédié, chaque service qui résout un CAPTCHA duplique la même mécanique : requête, interrogation de res.php, timeout, tri des codes d'erreur. Le jour où l'API évolue, vous corrigez le même bug dans cinq dépôts. Un paquet captchaai centralise cette logique :

  • Une seule source de vérité pour les URL, les codes d'erreur et le polling.
  • Des méthodes typées par famille de CAPTCHA, vérifiées à la compilation plutôt qu'à l'exécution.
  • Un http.Client injectable pour brancher un proxy ou un transport instrumenté sans toucher au code appelant.
  • Un binaire autonome, sans dépendance externe : vos workers restent légers à déployer.

Arborescence du paquet

captchaai/
├── client.go       # Main client and solve logic
├── errors.go       # Error types
├── types.go        # Request/response structs
└── client_test.go  # Tests

Définir les types d'erreurs

Commencez par les erreurs : elles déterminent votre logique de retry. Un APIError porte le code renvoyé par l'API, et sa méthode IsFatal() distingue les codes définitifs (clé invalide, solde nul) des erreurs transitoires. Le TimeoutError signale, lui, qu'une résolution a dépassé le délai configuré.

// 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)
}

Les structures de requête et de réponse

Chaque type de CAPTCHA a ses propres paramètres. Plutôt que d'empiler des map[string]string, exposez une structure typée par famille — RecaptchaV2Params, TurnstileParams, ImageParams — pour que le compilateur détecte les champs manquants avant l'exécution. Les options fonctionnelles suivent le patron idiomatique Go.

// 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"`
}

Le cœur du client

Le client encapsule deux appels : submit envoie la tâche sur in.php et récupère un identifiant, poll interroge res.php jusqu'à obtenir le token ou dépasser le délai. Le select sur ctx.Done(), la deadline et l'intervalle d'interrogation rend chaque résolution annulable. Les méthodes SolveRecaptchaV2, SolveRecaptchaV3, SolveTurnstile et SolveImage réutilisent ce socle.

// 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)
}

Mettre le client Go à l'œuvre

Voici le paquet en situation : on instancie le client, on vérifie le solde, puis on résout un reCAPTCHA v2 et un Turnstile. Le second appel utilise context.WithTimeout pour plafonner sa durée indépendamment du timeout global — pratique quand un handler HTTP a son propre budget de latence.

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])
}

Lancer plusieurs résolutions en parallèle

Chaque méthode Solve… est bloquante mais indépendante, idéale pour la concurrence native de Go. Pour traiter un lot d'URL, encapsulez les appels dans un errgroup.Group : la première erreur annule le context.Context partagé et les résolutions en cours s'arrêtent proprement. Deux garde-fous à retenir :

  • Le plafond de threads de votre plan. Le parallélisme réel est borné par les threads souscrits, pas par le nombre de goroutines. Dimensionnez votre pool en conséquence.
  • Le budget de latence. Donnez à chaque résolution un context.WithTimeout dérivé du contexte parent, pour qu'un CAPTCHA lent ne bloque pas tout le lot.

Déployer le client sur un worker

Le binaire unique de Go simplifie la mise en production sur un worker Scaleway, une instance OVHcloud ou une région AWS proche de vos utilisateurs (eu-west-3 à Paris). Le schéma tient en trois étapes :

  1. Compilez pour la cible avec CGO_ENABLED=0 GOOS=linux go build, ce qui produit un binaire statique sans dépendance système.
  2. Passez la clé API par variable d'environnement plutôt qu'en dur, et lisez-la au démarrage avant d'appeler captchaai.New.
  3. Ajustez WithTimeout et WithPollInterval au budget de latence du worker, puis coupez court dès qu'un code d'erreur définitif remonte.

Si le worker journalise les requêtes, limitez les données personnelles consignées et vérifiez vos obligations RGPD avant de conserver des URL ou des cookies.

Étendre le paquet à d'autres types de CAPTCHA

Le socle submit/poll est agnostique du type de CAPTCHA : ajouter une famille revient à écrire une structure de paramètres et une méthode Solve…. Via ce même patron, CaptchaAI prend en charge d'autres types au-delà des quatre déjà câblés :

  • reCAPTCHA Enterprise et Cloudflare Challenge.
  • GeeTest v3, en fournissant le challenge et le gt attendus.
  • Les grilles d'images et BLS CAPTCHA, pour les portails de rendez-vous en environnement autorisé.

Dépannage

Problème Cause Correctif
context deadline exceeded La résolution a dépassé le délai du contexte Allongez le timeout du contexte ou augmentez WithTimeout sur le client
captchaai: submit failed (ERROR_ZERO_BALANCE) Solde insuffisant Rechargez votre compte depuis le tableau de bord CaptchaAI
L'interrogation ne se termine jamais Problème réseau ou URL d'API erronée Vérifiez la connectivité et les constantes d'URL (submitURL, resultURL)
Erreur du compilateur sur errors.As Import manquant Ajoutez "errors" à la liste des imports
Le http.Client personnalisé est ignoré Option WithHTTPClient oubliée Passez-la à New() : captchaai.New(key, captchaai.WithHTTPClient(myClient))

FAQ

Quels types de CAPTCHA ce client Go peut-il résoudre ?

Les méthodes typées couvrent reCAPTCHA v2 et v3, Cloudflare Turnstile et les CAPTCHA image/OCR ; côté API, CaptchaAI prend aussi en charge reCAPTCHA Enterprise, Cloudflare Challenge, GeeTest v3, les grilles d'images et BLS, qu'il suffit d'ajouter en suivant le même patron. En revanche, hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge.

Comment résoudre plusieurs CAPTCHA en parallèle ?

Chaque appel Solve… est indépendant et bloquant, donc lancez-les dans des goroutines séparées avec un errgroup ou un sync.WaitGroup. Le parallélisme réel est plafonné par les threads de votre plan : le plus petit, BASIC ($15/mois, 5 threads), autorise cinq résolutions simultanées. Dimensionnez votre pool de goroutines en conséquence.

Comment faire passer le trafic par un proxy ?

Injectez un http.Client doté d'un transport proxy via WithHTTPClient. Tout le trafic du paquet emprunte alors votre proxy — résidentiel ou datacenter — sans modifier la bibliothèque.

Comment distinguer une erreur définitive d'une erreur à réessayer ?

Utilisez errors.As pour extraire un *APIError, puis appelez IsFatal(). Les codes comme ERROR_ZERO_BALANCE ou ERROR_KEY_DOES_NOT_EXIST sont définitifs : inutile de réessayer. Les autres erreurs (réseau, CAPCHA_NOT_READY) sont transitoires et se prêtent à un backoff exponentiel avant une nouvelle tentative.

Comment tester ce client sans consommer de crédits API ?

Injectez un http.Client pointant vers un serveur httptest qui renvoie des réponses in.php et res.php simulées. Vous validez ainsi le polling, le déclenchement du timeout et le tri des erreurs dans client_test.go, sans requête réelle ni dépense de solde.

Articles connexes

Prochaines étapes

Récupérez votre clé API CaptchaAI et démarrez avec le paquet ci-dessus.

Guides associés :

Les commentaires sont désactivés pour cet article.