Primeros Pasos

Formatos de respuesta API CaptchaAI explicados

Toda respuesta de la API de CaptchaAI cae en uno de tres casos: empieza por OK|, es exactamente CAPCHA_NOT_READY, o es un código ERROR_. Si tu código distingue esos tres casos, ya sabe hablar con la API entera: envío, sondeo, saldo y reportes comparten el mismo esquema de texto plano.

Esta referencia recorre endpoint por endpoint el cuerpo exacto que recibirás y cómo analizarlo en Python y JavaScript.

Las tres formas que puede tener una respuesta

Cuerpo recibido Qué significa Qué debe hacer tu código
OK\|... Todo bien; el dato viene tras la barra Separar una vez por \| y usar el payload
CAPCHA_NOT_READY La tarea sigue en proceso Esperar y volver a consultar
ERROR_... Fallo de configuración, saldo o tarea Clasificar el código y decidir si reintentas

El árbol es el mismo para reCAPTCHA v2, Cloudflare Turnstile, GeeTest v3 o un CAPTCHA de imagen: solo cambia el payload que va detrás de OK|.

Endpoint de envío (in.php)

Cuando envías una tarea, in.php contesta con una sola línea. Si entra en cola, el cuerpo es OK| seguido del identificador:

OK|TASK_ID

En la práctica: OK|73548291. Cuando algo falla no hay prefijo, solo el código pelado:

ERROR_CODE

Por ejemplo ERROR_WRONG_USER_KEY, clave no válida. Distinguir ambos casos es mirar los tres primeros caracteres:

resp = requests.get("https://ocr.captchaai.com/in.php", params={...})

if resp.text.startswith("OK|"):
    task_id = resp.text.split("|")[1]
else:
    error = resp.text
    raise Exception(f"Submit failed: {error}")
const resp = await axios.get("https://ocr.captchaai.com/in.php", { params });

if (resp.data.startsWith("OK|")) {
  const taskId = resp.data.split("|")[1];
} else {
  throw new Error(`Submit failed: ${resp.data}`);
}

Guarda el TASK_ID: es el único identificador para recuperar el resultado o reportarlo después.

Endpoint de consulta (res.php)

Mientras la tarea se procesa, res.php devuelve una única palabra:

CAPCHA_NOT_READY

No es un error: espera 5 segundos y vuelve a consultar el resultado. Sondear en bucle cerrado no acelera nada y consume tus threads.

Éxito — CAPTCHA de token

Para reCAPTCHA v2 y v3 y para Cloudflare Turnstile recibes un único token largo:

OK|03AGdBq24PBCbw...long_token_string

Ese valor va al campo que espera tu formulario: g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile.

Éxito — CAPTCHA de imagen y OCR

OK|abc123

El texto después de OK| es el texto reconocido en la imagen.

Éxito — GeeTest v3

OK|challenge:abc123,validate:def456,seccode:ghi789

Aquí el payload trae tres campos separados por comas. Divídelos y envía los tres:

if result.text.startswith("OK|"):
    data = result.text.split("|")[1]
    parts = dict(item.split(":") for item in data.split(","))
    challenge = parts["challenge"]
    validate = parts["validate"]
    seccode = parts["seccode"]

Éxito — Cloudflare Challenge

Devuelve el valor de la cookie de validación junto con el user agent asociado:

OK|qa_validation_cookie=abc123;user_agent=Mozilla/5.0...

Los dos valores van juntos: con otro user agent, la verificación no cuadra.

Respuesta de error

ERROR_CODE

Plantilla de análisis reutilizable

def parse_result(response_text):
    if response_text == "CAPCHA_NOT_READY":
        return {"status": "pending"}

    if response_text.startswith("OK|"):
        return {"status": "solved", "result": response_text.split("|", 1)[1]}

    return {"status": "error", "error": response_text}

Fíjate en split("|", 1): dividir una sola vez evita partir un token que contenga barras.

Consultar el saldo desde la misma API

GET https://ocr.captchaai.com/res.php?key=API_KEY&action=getbalance

Respuesta:

1.234

Un número decimal con tu saldo en USD, sin JSON alrededor: conviértelo a float y listo.

balance = float(requests.get("https://ocr.captchaai.com/res.php", params={
    "key": API_KEY, "action": "getbalance"
}).text)
print(f"Balance: ${balance:.2f}")

Reportar resoluciones correctas e incorrectas

Reportar una resolución correcta

GET https://ocr.captchaai.com/res.php?key=API_KEY&action=reportgood&id=TASK_ID

Respuesta: OK_REPORT_RECORDED

Reportar una resolución incorrecta

GET https://ocr.captchaai.com/res.php?key=API_KEY&action=reportbad&id=TASK_ID

Respuesta: OK_REPORT_RECORDED

Reportar las resoluciones que no sirvieron mejora la precisión y puede acreditar saldo a tu cuenta.

Respuestas de error de la API y qué hacer con cada una

Código de error Significado Acción
ERROR_WRONG_USER_KEY Clave API no válida Verifica tu clave
ERROR_KEY_DOES_NOT_EXIST Clave no registrada Revisa el panel de control
ERROR_ZERO_BALANCE Fondos insuficientes Añade saldo
ERROR_NO_SLOT_AVAILABLE Servidor a capacidad Reintenta pasados 5 segundos
ERROR_CAPTCHA_UNSOLVABLE Desafío demasiado difícil Reintenta con un CAPTCHA nuevo
ERROR_BAD_DUPLICATES Tarea duplicada rechazada Espera antes de reenviar
ERROR_WRONG_CAPTCHA_ID ID de tarea no válido Comprueba el valor del ID
ERROR_EMPTY_ACTION Falta el parámetro action Añade action=get
IP_BANNED Demasiadas solicitudes incorrectas Corrige tu clave API y espera

Agrúpalos en dos cajones: configuración o saldo (ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE, IP_BANNED), donde reintentar no arregla nada, y transitorios (ERROR_NO_SLOT_AVAILABLE), que se resuelven con retroceso exponencial. La lista completa está en la referencia de códigos de error.

Ejemplo completo de envío y sondeo

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_captcha(submit_params, timeout=300):
    """Generic solver with proper response handling."""
    submit_params["key"] = API_KEY

    # Submit
    resp = requests.get("https://ocr.captchaai.com/in.php", params=submit_params)
    if not resp.text.startswith("OK|"):
        raise Exception(f"Submit error: {resp.text}")

    task_id = resp.text.split("|")[1]

    # Poll
    deadline = time.time() + timeout
    while time.time() < deadline:
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id
        })

        parsed = parse_result(result.text)

        if parsed["status"] == "pending":
            continue
        elif parsed["status"] == "solved":
            return parsed["result"]
        else:
            raise Exception(f"Solve error: {parsed['error']}")

    raise TimeoutError(f"Task {task_id} timed out after {timeout}s")

Un escenario habitual en equipos hispanohablantes

Piensa en una agencia de Bogotá que mantiene el portal de trámites de un cliente, protegido por reCAPTCHA v2. Cada noche su suite rellena ese formulario en staging: pide el token a CaptchaAI desde el runner de CI y lo inyecta antes de enviar. Lo que decide si esa suite es estable no es la resolución en sí, sino el manejo de respuestas: si el runner toma CAPCHA_NOT_READY por un error, la build falla sin motivo; si trata ERROR_ZERO_BALANCE como transitorio, reintenta media hora contra una cuenta sin saldo. Con el plan BASIC ($15/mes, 5 threads) esa suite nocturna cabe de sobra y el coste queda fijo en USD. Aplícalo solo a aplicaciones que tú controlas y respeta la normativa de protección de datos aplicable (GDPR y LOPDGDD en España, LFPDPPP en México).

Mantén idéntica la configuración del navegador

Igualar viewport, idioma y user-agent en QA, staging y CI es la forma más barata de evitar que un test pase en local y falle en el runner:

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)

Qué medir y qué revisar cuando algo se tuerce

Instrumenta los pasos que dependen del CAPTCHA: tiempo de resolución por intento, tasa de éxito de tus propios endpoints, distribución de errores por código y latencia extremo a extremo. Con esas cuatro series, cada síntoma tiene una revisión asociada:

Síntoma Qué revisar
El test no detecta el widget Selectores y tiempos en tu entorno de staging
Llega ERROR_NO_SLOT_AVAILABLE Reintento con retroceso exponencial en tu pipeline
Tu backend rechaza el token action y sitekey frente a tu configuración real
Pasa en local y falla en CI Viewport, idioma y user-agent en ambos entornos
Tiempos de resolución muy variables Concurrencia y threads de tu plan

Dos hábitos evitan la mayoría de estos casos: una API key separada para QA, distinta de la de producción, y snapshots de configuración (sitekey, action, umbrales) versionados junto a los tests. Y prueba siempre sobre aplicaciones propias o entornos autorizados.

Preguntas frecuentes

¿Cada cuánto debo consultar res.php mientras la tarea está pendiente?

Cada 5 segundos, el intervalo que asume el ejemplo de arriba. Añade un límite de tiempo total para que una tarea colgada no bloquee el pipeline.

¿Qué errores debo reintentar y cuáles no?

Reintenta solo los transitorios, como ERROR_NO_SLOT_AVAILABLE, con retroceso exponencial. Los de clave, saldo o parámetros (ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE, ERROR_EMPTY_ACTION) exigen corregir la configuración: reintentarlos solo consume threads.

¿Necesito una API key distinta para cada tipo de CAPTCHA?

No. La misma clave sirve para todos los tipos compatibles; lo que cambia es el parámetro del método en el envío. Sí conviene separarlas por entorno.

¿Por qué mi token aparece cortado al analizarlo?

Casi siempre por dividir la respuesta sin límite. Usa split("|", 1) para quedarte con todo lo que sigue a la primera barra: los tokens de reCAPTCHA rondan los 500 caracteres.

Guías relacionadas

Valida tus integraciones de CAPTCHA en entornos propios con CaptchaAI.

Los comentarios están deshabilitados para este artículo.