Un BLS CAPTCHA no se resuelve leyendo texto: hay que interpretar un código numérico y hacer clic en las celdas correctas de una cuadrícula de 3×3. Desde Node.js el ciclo son cuatro movimientos: capturas las nueve imágenes con Puppeteer, las envías en base64 junto al código de instrucción a in.php, consultas el resultado en res.php y haces clic en los índices que devuelve la API de CaptchaAI. Suele cerrarse en 5–15 segundos.
Es el patrón habitual en equipos que mantienen integraciones con portales de cita previa tipo BLS, muy usados por solicitantes de visado de Latinoamérica y España.
Qué necesitas antes de empezar
| Elemento | Valor |
|---|---|
| Clave API de CaptchaAI | Desde captchaai.com |
| Node.js | 14+ |
| Biblioteca | axios (npm install axios) |
Añade puppeteer si controlas el navegador desde el mismo script, y guarda la clave API en una variable de entorno.
Cómo está numerada la cuadrícula de BLS CAPTCHA
Las nueve celdas van de izquierda a derecha y de arriba a abajo:
1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9
El widget muestra un código numérico — por ejemplo, "664" — que indica qué seleccionar. CaptchaAI recibe las imágenes más ese código y devuelve los índices coincidentes.
Paso 1: capturar las nueve imágenes de la cuadrícula
Lee el código de instrucción y convierte cada celda a base64. Unas implementaciones sirven las imágenes embebidas como data: y otras como URL:
const axios = require('axios');
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/bls-form');
// Get instruction code
const instruction = await page.$eval('.bls-instruction', (el) => el.textContent.trim());
// Get all 9 cell image URLs and convert to base64
const cellImages = await page.$$eval('.bls-grid img', (imgs) =>
imgs.map((img) => img.src)
);
const images = [];
for (const src of cellImages) {
if (src.startsWith('data:')) {
images.push(src);
} else {
const { data } = await axios.get(src, { responseType: 'arraybuffer' });
const b64 = Buffer.from(data).toString('base64');
images.push(`data:image/png;base64,${b64}`);
}
}
Si cellImages no trae nueve elementos, la página aún no terminó de renderizar.
Paso 2: enviar la tarea a la API de CaptchaAI
El envío va por POST a in.php con method: 'bls', el código de instrucción y las imágenes numeradas de image_base64_1 a image_base64_9. Con json: '1' la respuesta llega estructurada:
const API_KEY = 'YOUR_API_KEY';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const params = new URLSearchParams({
key: API_KEY,
method: 'bls',
instructions: instruction,
json: '1',
});
// Add all 9 images
images.forEach((img, i) => {
params.append(`image_base64_${i + 1}`, img);
});
const { data: submitData } = await axios.post(
'https://ocr.captchaai.com/in.php',
params.toString()
);
if (submitData.status !== 1) throw new Error(submitData.request);
const taskId = submitData.request;
console.log(`Task submitted: ${taskId}`);
Sustituye YOUR_API_KEY por tu clave real. Si status no es 1, request trae el código de error: léelo antes de reintentar.
Paso 3: consultar el resultado en res.php
El resultado no es inmediato. Espera cinco segundos y consulta res.php en bucle hasta recibirlo:
await sleep(5000);
let selectedCells;
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) {
selectedCells = JSON.parse(pollData.request);
console.log('Selected cells:', selectedCells);
break;
}
if (pollData.request !== 'CAPCHA_NOT_READY') {
throw new Error(pollData.request);
}
await sleep(5000);
}
CAPCHA_NOT_READY — escrito así, sin la "t" — significa "sigue esperando"; cualquier otro valor es un error real y corta el bucle.
Paso 4: hacer clic en las celdas devueltas
La API cuenta los índices desde 1 y el array del DOM desde 0. Ese desfase es el fallo más habitual del tutorial:
// Click each identified cell
const gridCells = await page.$$('.bls-grid img');
for (const cellNum of selectedCells) {
await gridCells[cellNum - 1].click();
}
// Submit the form
await page.click('.bls-submit');
console.log(`Solved: clicked cells ${JSON.stringify(selectedCells)}`);
await browser.close();
Salida esperada:
Selected cells: [1, 4, 7, 8]
Solved: clicked cells [1,4,7,8]
Errores frecuentes y qué hacer con cada uno
| Código | Causa | Qué hacer |
|---|---|---|
ERROR_BAD_PARAMETERS |
Faltan imágenes o el código de instrucción | Envía las nueve imágenes y el código completo |
CAPCHA_NOT_READY |
Tarea aún en proceso | Sigue el sondeo cada 5 segundos |
ERROR_ZERO_BALANCE |
Cuenta sin saldo | Recarga tu cuenta de CaptchaAI |
ERROR_NO_SLOT_AVAILABLE |
Sin threads libres | Reintenta con backoff exponencial o sube de plan |
Cuántos threads necesitas y cuánto cuesta
CaptchaAI factura por thread concurrente, no por resolución: cada plan incluye resoluciones ilimitadas dentro de sus threads, y un thread es un CAPTCHA en vuelo.
- BASIC ($15/mes, 5 threads): revisión secuencial de unos pocos formularios.
- STANDARD ($30/mes, 15 threads): varios entornos de QA en paralelo.
- ADVANCE ($90/mes, 50 threads): suites en cada despliegue.
Si facturas en una moneda volátil, un costo mensual fijo en USD es más fácil de presupuestar que el pago por resolución.
Configuración de navegador reproducible
Usa la misma configuración en local, staging y CI para evitar el clásico "en mi máquina funciona":
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)
Un viewport, un idioma y un user-agent idénticos reducen la varianza entre ejecuciones.
Cómo encaja CaptchaAI en tu pipeline
- Tu test detecta el widget en una página de tu propia aplicación (QA, staging, preproducción).
- Envía a CaptchaAI los datos públicos del widget: imágenes, código de instrucción y tipo de CAPTCHA.
- CaptchaAI devuelve la solución, aquí los índices de las celdas.
- Tu test la aplica y envía el formulario.
- Tu backend valida el envío igual que con una persona.
Este flujo aplica solo a integraciones que tú controlas, respetando los términos de servicio del portal y la normativa de protección de datos aplicable.
Qué medir y qué cuidar en QA
- Tiempo de resolución por intento y latencia extremo a extremo, incluido el render.
- Distribución de errores por código (
ERROR_*, timeouts, fallos de red). - Clave API de QA separada de la de producción, y selectores versionados con los tests.
Diagnóstico rápido
| Síntoma | Acción recomendada |
|---|---|
| No encuentra las nueve celdas | Espera al render antes de leer .bls-grid img |
| Los clics caen en celdas equivocadas | Revisa el desfase: la API cuenta desde 1 |
| Falla en CI y funciona en local | Iguala viewport, idioma y user-agent |
Preguntas frecuentes
¿En qué formato tengo que enviar las nueve imágenes?
En base64, una por parámetro, de image_base64_1 a image_base64_9 y en el orden de la cuadrícula. Si la página ya las sirve como data:, reenvíalas tal cual.
¿Qué pasa si el código de instrucción tiene más o menos dígitos?
Envíalo tal como aparece, sin normalizarlo: el campo instructions acepta el código completo.
¿Cuántos threads necesito para revisar varios formularios a la vez?
Uno por cada CAPTCHA simultáneo. Con BASIC ($15/mes, 5 threads) tienes cinco resoluciones en vuelo; por encima verás ERROR_NO_SLOT_AVAILABLE.
¿Qué otros tipos de CAPTCHA cubre CaptchaAI en estos portales?
Además de las cuadrículas de imagen tipo BLS: reCAPTCHA v2 y v3, Cloudflare Turnstile y Challenge, GeeTest v3 y OCR de texto. GeeTest v4 figura como próximamente; CaptchaFox, Friendly Captcha y Lemin están en beta.
Guías relacionadas
Empieza a resolver BLS CAPTCHA con CaptchaAI
Valida tus integraciones de CAPTCHA en entornos propios con CaptchaAI.