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
- Cómo configurar y autenticar tu API key
- Referencia de códigos de error de CaptchaAI
- Inicio rápido: tu primera resolución
Valida tus integraciones de CAPTCHA en entornos propios con CaptchaAI.