El trabajo de visión no lo hace tu script. Recortas la cuadrícula con Puppeteer, la envías a la API de CaptchaAI junto con el texto de la instrucción y recibes un array con las celdas que hay que pulsar. Tu código solo captura y hace clic.
El desafío típico es una rejilla de 3×3 o 4×4 con una frase como "selecciona todos los cuadros con semáforos": la instrucción importa tanto como la imagen. Estos son los cuatro pasos, en Node.js.
Qué necesitas antes de empezar
| Elemento | Valor |
|---|---|
| Clave API de CaptchaAI | Desde captchaai.com |
| Node.js | 14+ |
| Bibliotecas | axios, puppeteer |
El plan BASIC cuesta $15/mes con 5 threads y resoluciones ilimitadas. Un thread es un CAPTCHA en vuelo: 5 threads dan cinco cuadrículas simultáneas, sin tarifa por resolución.
Paso 1: captura la cuadrícula con Puppeteer
El desafío vive en un iframe aparte (recaptcha/api2/bframe), no en el widget que ves primero. Localiza ese frame, lee la instrucción y recorta la captura al contenedor de mosaicos: la página entera añade ruido.
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' });
La captura se toma sobre .rc-imageselect-target, que envuelve exactamente los mosaicos. Ese recorte es la mitad del trabajo.
Paso 2: envía la imagen a la API de CaptchaAI
La solicitud es un multipart/form-data contra in.php. Los campos que marcan la diferencia son grid_size, img_type con valor recaptcha y, sobre todo, instructions. Con json=1 la respuesta llega estructurada.
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}`);
Si status no vale 1, request trae el código de error (ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE). Lanza la excepción ahí mismo en vez de sondear un identificador inexistente.
Paso 3: sondea el resultado
La resolución no es instantánea. Espera unos cinco segundos y consulta res.php a intervalos regulares. Mientras la tarea sigue en cola, la API responde CAPCHA_NOT_READY — ese literal se escribe así, sin la "t", y cuesta horas de depuración a quien lo da por corregido.
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);
}
El resultado es un array de celdas indexadas desde 1, leídas de izquierda a derecha y de arriba abajo. Cualquier otro valor en request es un error real.
Paso 4: pulsa los mosaicos y verifica
La única trampa: la API numera las celdas desde 1 y el array del DOM empieza en 0, de ahí el -1. La pausa de 300 ms evita que el widget descarte pulsaciones demasiado seguidas.
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();
Salida esperada:
Click cells: [1, 3, 6, 9]
Solved: clicked tiles [1,3,6,9]
Un caso real: portales de cita previa y trámites públicos
Los centros de visados BLS, los trámites del SAT o los portales de cita previa comparten un problema: son formularios públicos con desafíos de imágenes que rompen los tests de regresión del equipo que los mantiene.
El patrón que funciona ahí es reservar un par de threads para CI y dejar el resto para staging. BASIC ($15/mes, 5 threads) cubre una suite nocturna; STANDARD ($30/mes, 15 threads) da margen si lanzas pruebas en paralelo por cada pull request. Los precios se facturan en USD.
Ejecuta esto contra tu propia aplicación o entornos donde tengas autorización explícita, y respeta los términos de servicio y la normativa de protección de datos aplicable (GDPR y LOPDGDD, LFPDPPP en México).
Iguala el navegador entre local, staging y CI
El fallo más frustrante no es el CAPTCHA: es el test que pasa en tu máquina y falla en el runner porque el navegador no arranca igual. Fija viewport, idioma y modo headless en una única función y úsala en todos los entornos.
from selenium import webdriver
def make_driver(headless: bool = True) -> webdriver.Chrome:
options = webdriver.ChromeOptions()
if headless:
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,800')
options.add_argument('--lang=es-ES')
return webdriver.Chrome(options=options)
El fragmento está en Python a propósito: la disciplina vale para cualquier arnés de pruebas. Mide además el tiempo de resolución por intento y separa la clave API de QA de la de producción.
Cuando algo falla
| Síntoma | Acción recomendada |
|---|---|
| No encuentra el iframe | Revisa el filtro por bframe |
ERROR_NO_SLOT_AVAILABLE |
Reintenta con backoff; tus threads están ocupados |
| Se pulsan mosaicos equivocados | Comprueba el -1 y que grid_size coincida con la rejilla |
| El sondeo no termina | Confirma que comparas contra CAPCHA_NOT_READY exactamente |
| Pasa en local, falla en CI | Iguala viewport, idioma y user-agent |
Preguntas frecuentes
¿Hace falta enviar el texto de la instrucción o basta con la imagen?
Envíalo siempre. instructions es lo que distingue "selecciona los semáforos" de "selecciona los pasos de peatones" sobre la misma fotografía.
¿Sirve este mismo código para otros tipos de desafío?
No para todos. CaptchaAI no es compatible con hCaptcha ni con FunCaptcha (Arkose Labs). Sí están cubiertos reCAPTCHA v2 y v3, Cloudflare Turnstile y Challenge, GeeTest v3, imagen/OCR y grid image; CaptchaFox, Friendly Captcha y Lemin siguen en beta, y GeeTest v4 figura como próximamente.
¿Cuántos threads necesito para una suite de pruebas nocturna?
Un thread resuelve un CAPTCHA a la vez: cuenta cuántos necesitas en vuelo. BASIC ($15/mes) trae 5 threads y basta para el CI de un proyecto; ADVANCE ($90/mes, 50 threads) es el escalón cuando varios equipos comparten cuenta.
¿Puedo usar Playwright o Selenium en lugar de Puppeteer?
Sí. Las llamadas a la API son idénticas — in.php, res.php, los mismos parámetros. Solo cambia cómo localizas el iframe y pulsas los mosaicos.
Guías relacionadas
Empieza a resolver cuadrículas de imágenes con CaptchaAI →
Valida tus integraciones sobre entornos que controlas con CaptchaAI.