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.Clientinjectable 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.WithTimeoutdé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 :
- Compilez pour la cible avec
CGO_ENABLED=0 GOOS=linux go build, ce qui produit un binaire statique sans dépendance système. - Passez la clé API par variable d'environnement plutôt qu'en dur, et lisez-la au démarrage avant d'appeler
captchaai.New. - Ajustez
WithTimeoutetWithPollIntervalau 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
challengeet legtattendus. - 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
- Sécuriser vos clés API par liste blanche d'IP
- La rotation des clés API CaptchaAI
- Cartographie des endpoints de l'API face aux concurrents
Prochaines étapes
Récupérez votre clé API CaptchaAI et démarrez avec le paquet ci-dessus.
Guides associés :