Un registro de auditoría es la respuesta a una pregunta que siempre llega tarde: "¿por qué el mes pasado se resolvieron 40.000 CAPTCHA y de dónde salió ese gasto?". Si cada resolución deja una entrada bien estructurada, respondes en segundos lo que de otro modo te costaría reconstruir una mañana entera:
- ¿Quién o qué inició la resolución?
- ¿Para qué sitio fue?
- ¿Cuánto costó y cuánto tardó?
- ¿En qué momento ocurrió?
Esta guía muestra cómo construir ese registro para tus operaciones con CaptchaAI, campo por campo y con código listo para producción en Python y JavaScript.
Qué campos capturar en cada resolución
Cada resolución CAPTCHA debería dejar constancia de estos campos:
| Campo | Para qué sirve | Ejemplo |
|---|---|---|
timestamp |
Momento en que se hizo la solicitud | 2026-04-04T14:30:00Z |
request_id |
Identificador único de esta resolución | uuid4() |
captcha_type |
Método CAPTCHA utilizado | userrecaptcha |
target_site |
URL de la página que se está resolviendo | https://staging.example.com/qa-login |
task_id |
ID de tarea de CaptchaAI | 73829451 |
status |
Resultado | solved, failed, timeout |
solve_time_ms |
Tiempo desde el envío hasta el resultado | 18432 |
error_code |
Error en caso de fallo | ERROR_CAPTCHA_UNSOLVABLE |
initiator |
Quién o qué inició la resolución | scraper-job-42 |
cost |
Coste estimado | 0.003 |
Qué no registrar nunca
Nunca lleves al registro:
- claves API
- tokens CAPTCHA (son efímeros)
- información de identificación personal de los sitios de destino
Este último punto no es solo higiene: el registro debe demostrar actividad sin acumular datos personales que después tengas que proteger. Anota la URL y el resultado; deja fuera cualquier dato del usuario final.
Registro de auditoría en Python
# audit_solver.py
import os
import uuid
import time
import json
import logging
from datetime import datetime, timezone
import requests
API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")
# Configure audit logger — separate from application logs
audit_logger = logging.getLogger("captcha_audit")
audit_logger.setLevel(logging.INFO)
# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
"captcha_audit.jsonl",
maxBytes=50_000_000, # 50 MB per file
backupCount=10,
)
handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(handler)
def log_audit(record):
"""Write a structured audit record."""
audit_logger.info(json.dumps(record, default=str))
def solve_with_audit(sitekey, pageurl, captcha_type="userrecaptcha",
initiator="unknown"):
"""Solve a CAPTCHA with full audit logging."""
request_id = str(uuid.uuid4())
start = time.time()
audit_record = {
"request_id": request_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"captcha_type": captcha_type,
"target_site": pageurl,
"initiator": initiator,
"status": "submitted",
}
session = requests.Session()
try:
# Submit
resp = session.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": captcha_type,
"googlekey": sitekey,
"pageurl": pageurl,
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
audit_record.update({
"status": "submit_failed",
"error_code": result.get("request"),
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
task_id = result["request"]
audit_record["task_id"] = task_id
# Poll
time.sleep(15)
for _ in range(25):
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
})
poll_result = poll.json()
if poll_result.get("status") == 1:
solve_time = int((time.time() - start) * 1000)
audit_record.update({
"status": "solved",
"solve_time_ms": solve_time,
"cost_estimate": 0.003, # Adjust per your rate
})
log_audit(audit_record)
return poll_result["request"]
if poll_result.get("request") != "CAPCHA_NOT_READY":
audit_record.update({
"status": "failed",
"error_code": poll_result.get("request"),
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
time.sleep(5)
audit_record.update({
"status": "timeout",
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
return None
except Exception as e:
audit_record.update({
"status": "error",
"error_code": str(e)[:200],
"solve_time_ms": int((time.time() - start) * 1000),
})
log_audit(audit_record)
raise
# Usage
token = solve_with_audit(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://www.google.com/recaptcha/api2/demo",
initiator="price-scraper-v2",
)
La clave está en usar un logger dedicado (captcha_audit), separado de los logs de la aplicación, y escribir el bloque de auditoría en cada rama del flujo: envío fallido, resuelto, fallido, timeout y excepción. Así ninguna resolución se queda sin rastro.
Anatomía de una entrada JSONL
{"request_id":"a1b2c3d4-...","timestamp":"2026-04-04T14:30:00+00:00","captcha_type":"userrecaptcha","target_site":"https://www.google.com/recaptcha/api2/demo","initiator":"price-scraper-v2","status":"solved","task_id":"73829451","solve_time_ms":18432,"cost_estimate":0.003}
Una línea por resolución, JSON en cada línea: es el formato que consumen sin fricción tanto jq como agregadores tipo ELK o Datadog.
El mismo registro en JavaScript
// audit_solver.js
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
const AUDIT_FILE = 'captcha_audit.jsonl';
function logAudit(record) {
fs.appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n');
}
async function solveWithAudit(sitekey, pageurl, initiator = 'unknown') {
const requestId = uuidv4();
const start = Date.now();
const record = {
request_id: requestId,
timestamp: new Date().toISOString(),
captcha_type: 'userrecaptcha',
target_site: pageurl,
initiator,
status: 'submitted',
};
try {
const submit = await axios.get('https://ocr.captchaai.com/in.php', {
params: {
key: API_KEY, method: 'userrecaptcha',
googlekey: sitekey, pageurl, json: '1',
},
});
if (submit.data.status !== 1) {
record.status = 'submit_failed';
record.error_code = submit.data.request;
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
}
record.task_id = submit.data.request;
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 25; i++) {
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
});
if (poll.data.status === 1) {
record.status = 'solved';
record.solve_time_ms = Date.now() - start;
record.cost_estimate = 0.003;
logAudit(record);
return poll.data.request;
}
if (poll.data.request !== 'CAPCHA_NOT_READY') {
record.status = 'failed';
record.error_code = poll.data.request;
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
}
await new Promise(r => setTimeout(r, 5000));
}
record.status = 'timeout';
record.solve_time_ms = Date.now() - start;
logAudit(record);
return null;
} catch (e) {
record.status = 'error';
record.error_code = e.message.slice(0, 200);
record.solve_time_ms = Date.now() - start;
logAudit(record);
throw e;
}
}
El patrón es idéntico al de Python: un objeto record que se va completando y se persiste con appendFileSync en cada salida del flujo. Con alto volumen, conviene mover esa escritura a un stream en modo append.
Consultas y resúmenes del registro
Un registro solo vale lo que valen las preguntas que puedes hacerle. Con el formato JSONL basta un script corto para sacar el pulso diario.
Resumen diario de actividad
import json
from collections import Counter
from datetime import date
def daily_summary(log_file, target_date=None):
"""Generate a daily summary from audit logs."""
target = target_date or date.today().isoformat()
statuses = Counter()
total_cost = 0
solve_times = []
with open(log_file) as f:
for line in f:
record = json.loads(line)
if record["timestamp"].startswith(target):
statuses[record["status"]] += 1
total_cost += record.get("cost_estimate", 0)
if record.get("solve_time_ms"):
solve_times.append(record["solve_time_ms"])
print(f"Date: {target}")
print(f"Total requests: {sum(statuses.values())}")
print(f"Statuses: {dict(statuses)}")
print(f"Estimated cost: ${total_cost:.2f}")
if solve_times:
print(f"Median solve time: {sorted(solve_times)[len(solve_times)//2]}ms")
daily_summary("captcha_audit.jsonl")
En una sola pasada obtienes el recuento por estado, el coste estimado del día y la mediana del tiempo de resolución. Es la base de un panel diario o de una alerta cuando la proporción de failed se dispara.
Errores frecuentes al registrar
| Problema | Causa | Solución |
|---|---|---|
| El archivo de registro crece demasiado | No hay rotación configurada | Usa RotatingFileHandler o logrotate |
| Faltan entradas de auditoría | Se lanza una excepción antes de registrar | Registra dentro del bloque finally |
| Escrituras lentas con alto volumen | I/O de archivo sincrónico | Usa escrituras asíncronas o un búfer |
| Marcas de tiempo inconsistentes | Desfase del reloj del sistema | Usa NTP y registra siempre en UTC |
Dónde y cuánto tiempo conservar los registros
El tamaño del registro escala de forma lineal con tu volumen, así que la estrategia de almacenamiento depende de cuántas resoluciones haces al día:
| Volumen | Tamaño diario del registro | Almacenamiento mensual | Recomendación |
|---|---|---|---|
| 100 resoluciones/día | ~30 KB | ~1 MB | Archivo local |
| 1.000 resoluciones/día | ~300 KB | ~10 MB | Archivo local + rotación |
| 10.000 resoluciones/día | ~3 MB | ~100 MB | Envío a un agregador de registros |
| 100.000 resoluciones/día | ~30 MB | ~1 GB | Registro centralizado (ELK, Datadog) |
Como referencia de retención, 90 días es lo habitual para registros operativos; para requisitos regulatorios, ajusta el plazo a lo que exija tu sector.
Cómo encaja en tu programa de cumplimiento
Para una agencia o un integrador que factura en USD por el consumo, este registro es tu fuente de verdad a la hora de cuadrar el gasto mensual con la factura de CaptchaAI y de repartir costes por cliente o por proyecto. Además, la disciplina de no guardar datos personales lo convierte en evidencia útil frente a marcos habituales en la región sin crearte un pasivo nuevo:
- GDPR y LOPDGDD (España y la UE)
- LFPDPPP (México) y normas equivalentes en Latinoamérica
- SOC 2, cuando el cliente exige controles operativos auditables
Respeta siempre los términos de servicio y la normativa de protección de datos aplicable; esto no es asesoramiento legal.
Preguntas frecuentes
¿Los registros de auditoría bastan para cumplir con GDPR o SOC 2?
No por sí solos. Son una evidencia sólida de qué se hizo, cuándo y con qué coste, pero el cumplimiento exige además controles de acceso, políticas de retención y trazabilidad de cambios. Trátalos como una pieza del expediente, no como el expediente completo.
¿Cómo cuadro el registro con la factura mensual de CaptchaAI?
Suma el campo cost_estimate de tus entradas del mes y compáralo con el panel de uso de CaptchaAI. Si ajustas ese valor a tu tarifa real, el resumen diario te da un contraste directo contra lo que verás en la factura.
¿Cómo evito que se pierdan entradas si una resolución lanza una excepción?
Registra en el bloque finally, no solo en las rutas de éxito. Así, aunque el flujo termine con un error inesperado, la resolución queda anotada con su estado error y su error_code.
¿Puedo detectar picos de coste anómalos con estos registros?
Sí. Al agrupar por initiator y por día ves qué proceso disparó el gasto. Un job que de pronto multiplica sus resoluciones aparece de inmediato en el resumen diario, antes de que llegue la factura.
Artículos relacionados
¿Listo para dejar rastro de cada resolución? Obtén tu API key de CaptchaAI y añade el registro de auditoría a tu primer flujo hoy mismo.
Guías relacionadas: