API Tutorials

Résoudre les CAPTCHA en grille d'images avec Node.js et CaptchaAI

Sur un CAPTCHA en grille d'images, l'API CaptchaAI ne renvoie aucun token à coller dans un champ caché. Elle renvoie une liste de numéros de cellules — [1, 3, 6, 9] — et c'est votre script Puppeteer qui doit la transformer en clics : au bon endroit, dans la bonne iframe, sans faire recharger le défi. Contrairement à reCAPTCHA v2 « case à cocher », la partie délicate se situe donc après la réponse de l'API, pas avant.

Ce guide déroule la séquence complète en Node.js, avec les pièges qui font échouer une intégration en production alors que la réponse de l'API, elle, était juste.


Ce que l'API renvoie réellement

L'endpoint d'images de CaptchaAI traite la grille comme une tâche OCR : vous postez un fichier PNG accompagné du texte de l'instruction (« sélectionnez tous les carrés contenant des feux de circulation ») et de la taille de la grille. La réponse n'est pas une image annotée mais un tableau JSON du type [1, 3, 6, 9], numéroté de gauche à droite puis de haut en bas, à partir de 1.

Trois conséquences pour votre code :

  • L'indexation démarre à 1, alors que votre tableau de vignettes DOM démarre à 0. L'écart d'une unité est la cause la plus fréquente de clics à côté.
  • L'instruction compte autant que l'image. Une capture envoyée sans le champ instructions donne un résultat inexploitable : rien n'indique quel objet chercher dans les vignettes.
  • La grille doit être capturée nette et entière. Une capture rognée par un défilement de page fausse le découpage des cellules.

Côté vitesse, la grille d'images est un type rapide : les mesures internes situent la résolution à < 1 s, ce qui rend le premier appel de polling utile dès quelques secondes après l'envoi.


Prérequis

Élément Valeur
Clé API CaptchaAI Depuis captchaai.com
Node.js 14+
Bibliothèques axios, puppeteer

Prévoyez aussi un conteneur muni des dépendances Chromium : Puppeteer en mode headless capture l'iframe du défi.


Étape 1 : capturez la grille et son instruction

Le défi vit dans une iframe distincte de la case à cocher. Vous devez donc localiser la frame bframe, y lire le texte de consigne, puis prendre une capture ciblée du conteneur de la grille — jamais de la page entière.

const puppeteer = require('puppeteer');
const fs = require('fs');

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/page-with-recaptcha');

// Switch to the reCAPTCHA challenge iframe
const frames = page.frames();
const challengeFrame = frames.find((f) => f.url().includes('recaptcha/api2/bframe'));

// Get the instruction text
const instruction = await challengeFrame.$eval(
  '.rc-imageselect-desc-no-canonical',
  (el) => el.textContent.trim()
);

// Screenshot the grid
const grid = await challengeFrame.$('.rc-imageselect-target');
await grid.screenshot({ path: 'grid.png' });

Si challengeFrame vaut undefined, la grille n'est pas encore affichée : attendez le sélecteur plutôt que d'ajouter un délai fixe.


Étape 2 : envoyez l'image à l'API CaptchaAI

L'envoi est un POST multipart classique. Les champs qui comptent sont grid_size, img_type et instructions ; le reste suit la convention in.php déjà utilisée par les autres types.

const axios = require('axios');
const FormData = require('form-data');

const API_KEY = 'YOUR_API_KEY';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const form = new FormData();
form.append('key', API_KEY);
form.append('method', 'post');
form.append('grid_size', '3x3');
form.append('img_type', 'recaptcha');
form.append('instructions', instruction);
form.append('json', '1');
form.append('file', fs.createReadStream('grid.png'));

const { data: submitData } = await axios.post('https://ocr.captchaai.com/in.php', form, {
  headers: form.getHeaders(),
});

if (submitData.status !== 1) throw new Error(submitData.request);
const taskId = submitData.request;
console.log(`Task submitted: ${taskId}`);

Conservez taskId dans une variable de portée suffisante : c'est la seule référence utilisable ensuite.


Étape 3 : interrogez le résultat jusqu'à la réponse

Le polling suit toujours le même contrat : CAPCHA_NOT_READY signifie « repassez plus tard », toute autre valeur est une erreur définitive qu'il ne sert à rien de retenter. Une boucle bornée évite qu'un incident réseau immobilise un worker.

await sleep(5000);

let cellsToClick;
for (let i = 0; i < 30; i++) {
  const { data: pollData } = await axios.get('https://ocr.captchaai.com/res.php', {
    params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
  });

  if (pollData.status === 1) {
    cellsToClick = JSON.parse(pollData.request);
    console.log('Click cells:', cellsToClick);
    break;
  }
  if (pollData.request !== 'CAPCHA_NOT_READY') {
    throw new Error(pollData.request);
  }
  await sleep(5000);
}

Journalisez taskId et la durée de chaque tâche : sans cette mesure, un ralentissement reste inexplicable.


Étape 4 : cliquez les vignettes puis validez

C'est ici que se joue la réussite. Espacez les clics de quelques centaines de millisecondes : la grille anime chaque vignette sélectionnée, et une rafale instantanée déclenche souvent un rechargement du défi avant même la validation.

const tiles = await challengeFrame.$$('.rc-imageselect-tile');

for (const cellNum of cellsToClick) {
  await tiles[cellNum - 1].click();
  await sleep(300);
}

// Click verify
await challengeFrame.click('#recaptcha-verify-button');
console.log(`Solved: clicked tiles ${JSON.stringify(cellsToClick)}`);
await browser.close();

Résultat attendu :

Click cells: [1, 3, 6, 9]
Solved: clicked tiles [1,3,6,9]

Le cellNum - 1 n'est pas cosmétique : il convertit la numérotation de l'API vers l'index du tableau DOM.


Exemple : un worker de QA hébergé en Europe

Cas typique : une plateforme e-commerce belge exécute ses tests de parcours de paiement sur une machine Scaleway à Paris, et la grille d'images apparaît une fois sur trois sur l'environnement de recette. Le pipeline tourne la nuit, sans personne devant l'écran.

Le montage qui tient dans la durée :

  • Un conteneur Chromium par worker, déployé sur OVHcloud ou Scaleway, avec la clé API injectée par variable d'environnement — jamais écrite dans le dépôt.
  • Une file d'attente en amont : chaque scénario de test pousse une tâche, les workers la consomment selon le nombre de threads disponibles sur votre plan.
  • Des captures purgées après usage. Une grille peut contenir des photos de rue ; côté RGPD, gardez-les le temps du diagnostic, pas davantage, et documentez cette durée dans votre registre de traitement.

Sur le dimensionnement : CaptchaAI facture des threads simultanés, pas des résolutions. Un thread traite une grille à la fois, puis reprend la suivante. Une suite de tests nocturne tient largement dans BASIC ($15/mois, 5 threads) ; une exécution en parallèle sur plusieurs environnements justifie STANDARD ($30/mois, 15 threads). La facturation est en dollars US.


Dépannage

Problème Cause probable Correctif
Les clics tombent à côté Index API (1 à 9) utilisé tel quel sur le tableau DOM Gardez tiles[cellNum - 1]
Réponse vide ou incohérente instructions non transmis ou tronqué Lisez le texte de la frame avant la capture et envoyez-le tel quel
CAPCHA_NOT_READY en boucle jusqu'au timeout Premier appel trop tardif ou tâche perdue Vérifiez taskId, gardez un intervalle de 5 s et une borne de 30 itérations
Le défi se recharge après validation Clics trop rapprochés, ou défi à plusieurs tours Conservez le délai de 300 ms et rejouez la boucle capture → envoi
challengeFrame vaut undefined La grille n'est pas encore rendue Attendez le sélecteur .rc-imageselect-target avant la capture

FAQ

Combien de temps prend la résolution d'une grille d'images ?

Les mesures internes situent ce type à < 1 s de résolution. Ajoutez le temps de capture et le premier intervalle de polling : en pratique, comptez quelques secondes entre le POST et le clic.

Que faire si res.php renvoie CAPCHA_NOT_READY sans jamais s'arrêter ?

Vérifiez d'abord que vous interrogez le bon taskId et que la boucle est bornée. Un intervalle de 5 s sur 30 itérations couvre largement ce type ; au-delà, relancez une capture propre.

Comment gérer une grille 4×4 ou un défi qui se recharge ?

Passez grid_size à 4x4 pour les grilles 4×4. Pour un défi à plusieurs tours, rejouez le cycle complet — capture, envoi, polling, clics — à chaque nouvelle série de vignettes.

CaptchaAI prend-il en charge hCaptcha pour ce genre de défi visuel ?

Non — hCaptcha n'est pas pris en charge, et FunCaptcha (Arkose Labs) non plus. Les grilles couvertes ici sont celles de reCAPTCHA, ainsi que les CAPTCHA image/OCR classiques.

Faut-il un proxy résidentiel pour que le clic soit accepté ?

Pas systématiquement. Sur un environnement de recette que vous contrôlez, un proxy n'apporte rien. Sur un site public, l'adresse IP et le comportement du navigateur pèsent plus que la résolution elle-même.


Guides associés


Créez votre compte et lancez votre premier envoi de grille →

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