Tutorials

Eventos enviados pelo servidor para notificações de resolução CAPTCHA em tempo real

Um painel que consulta res.php a cada 5 segundos entrega o token sempre atrasado: se a CaptchaAI terminou aos 12 s, o navegador só descobre aos 15 s.

Encurtar o intervalo só troca atraso por requisição desperdiçada. Inverta o sentido: aponte o pingback para o seu backend e mantenha aberto um stream de Server-Sent Events (SSE). Quando o callback chega, o servidor empurra o resultado pela conexão já de pé.

Quando o SSE compensa e quando é exagero

O resultado anda em um sentido só: do servidor para o cliente.

Critério SSE WebSocket Polling
Direção Servidor → cliente Bidirecional Cliente → servidor
Reconexão Nativa Manual N/A
Requisições à toa Nenhuma Nenhuma Muitas
Para resultado de CAPTCHA Recomendado Exagero Funciona, mas desperdiça

Troque por WebSocket só se o navegador precisar responder.

O caminho do token, do in.php até a tela

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. O navegador abre o stream SSE (conexão HTTP persistente)
  2. O backend envia a tarefa ao in.php com o pingback apontando para o próprio domínio
  3. A CaptchaAI resolve e chama o seu endpoint de callback
  4. O callback deposita o evento e o stream entrega ao navegador

A chave de API nunca sai do backend.

Servidor SSE em Python com Flask

Cada conexão ganha uma Queue; o handler de callback só precisa saber em qual fila depositar o token.

import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify

app = Flask(__name__)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()


@app.route("/events/<client_id>")
def sse_stream(client_id):
    """SSE endpoint — clients connect here for real-time results."""
    q = queue.Queue()

    with queues_lock:
        client_queues[client_id] = q

    def generate():
        try:
            while True:
                # Block until a result arrives (timeout for keepalive)
                try:
                    data = q.get(timeout=30)
                    yield f"event: captcha-solved\ndata: {data}\n\n"
                except queue.Empty:
                    # Send keepalive comment to prevent connection timeout
                    yield ": keepalive\n\n"
        finally:
            with queues_lock:
                client_queues.pop(client_id, None)

    return Response(
        generate(),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )


@app.route("/submit", methods=["POST"])
def submit_captcha():
    """Submit a CAPTCHA task with callback to this server."""
    data = request.json
    client_id = data["client_id"]
    sitekey = data["sitekey"]
    pageurl = data["pageurl"]

    callback_url = f"{request.host_url}callback?client_id={client_id}"

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    result = resp.json()

    if result.get("status") == 1:
        return jsonify({"task_id": result["request"]})
    return jsonify({"error": result.get("request")}), 400


@app.route("/callback")
def captcha_callback():
    """Receive CaptchaAI callback and push to SSE stream."""
    client_id = request.args.get("client_id")
    task_id = request.args.get("id")
    solution = request.args.get("code")

    import json
    message = json.dumps({
        "task_id": task_id,
        "solution": solution
    })

    with queues_lock:
        q = client_queues.get(client_id)
        if q:
            q.put(message)

    return "OK", 200


if __name__ == "__main__":
    app.run(port=5000, threaded=True)

Três detalhes decidem se isso sobrevive em produção: X-Accel-Buffering: no impede o buffer do nginx, o keepalive de 30 s evita que proxies fechem a conexão ociosa e o finally limpa as filas órfãs.

O cliente no navegador

O EventSource reconecta sozinho quando a rede oscila.

<!DOCTYPE html>
<html>
<body>
  <button onclick="submitCaptcha()">Solve CAPTCHA</button>
  <div id="results"></div>

  <script>
    const clientId = crypto.randomUUID();
    const resultsDiv = document.getElementById("results");

    // Connect SSE stream
    const eventSource = new EventSource(`/events/${clientId}`);

    eventSource.addEventListener("captcha-solved", (event) => {
      const data = JSON.parse(event.data);
      resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
    });

    eventSource.onerror = () => {
      console.log("SSE connection lost, reconnecting...");
    };

    async function submitCaptcha() {
      const response = await fetch("/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: clientId,
          sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
          pageurl: "https://example.com"
        })
      });
      const result = await response.json();
      resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
    }
  </script>
</body>
</html>

A mesma rota em Node.js com Express

Aqui não há fila: você guarda o Response de cada conexão aberta em um Map e escreve nele quando o callback chega.

const express = require("express");
const axios = require("axios");

const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";

// Per-client SSE connections: clientId -> Response object
const clients = new Map();

// SSE endpoint
app.get("/events/:clientId", (req, res) => {
  const clientId = req.params.clientId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  });

  clients.set(clientId, res);

  // Keepalive every 30 seconds
  const keepalive = setInterval(() => {
    res.write(": keepalive\n\n");
  }, 30000);

  req.on("close", () => {
    clearInterval(keepalive);
    clients.delete(clientId);
  });
});

// Submit CAPTCHA
app.post("/submit", async (req, res) => {
  const { client_id, sitekey, pageurl } = req.body;
  const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;

  try {
    const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: pageurl,
        pingback: callbackUrl,
        json: 1,
      },
    });

    if (resp.data.status === 1) {
      return res.json({ task_id: resp.data.request });
    }
    res.status(400).json({ error: resp.data.request });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
  const clientId = req.query.client_id;
  const taskId = req.query.id;
  const solution = req.query.code;

  const clientRes = clients.get(clientId);
  if (clientRes) {
    const data = JSON.stringify({ task_id: taskId, solution: solution });
    clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log("SSE server running on :3000"));

Produção: várias instâncias, limites e custo

Barramento entre o callback e o stream

Conexões SSE têm estado. Com duas instâncias atrás de um balanceador, o callback pode cair na instância B enquanto o navegador está preso à A, e o token some sem erro no log. Um Redis Pub/Sub resolve:

# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))

# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
    if msg["type"] == "message":
        yield f"data: {msg['data'].decode()}\n\n"

Limites, latência e escopo autorizado

Em HTTP/1.1 o navegador abre no máximo 6 conexões por domínio; com HTTP/2 esse teto some. Se os seus workers rodam em sa-east-1 (São Paulo), meça o RTT do callback no seu ambiente antes de calibrar o timeout do proxy.

Use o padrão em ambiente próprio, em staging e em QA autorizado, e considere a LGPD (RGPD, em Portugal) nos logs de callback.

O SSE não muda a fatura: os planos são cobrados por thread simultânea, com resoluções ilimitadas. O BASIC (US$ 15/mês, 5 threads) já sustenta um painel de QA; o ADVANCE (US$ 90/mês, 50 threads) atende suítes em paralelo.

Quando o evento não chega: diagnóstico

Sintoma Causa provável Correção
A conexão cai a cada 30 s Timeout do proxy Aumente o timeout do proxy
Nada chega ao navegador Callback em outra instância Redis Pub/Sub entre callback e stream
Erro de CORS no console Cabeçalho ausente Adicione Access-Control-Allow-Origin
Reconexões em sequência Evento mal formado Termine cada evento com linha em branco
Os tokens chegam em lote Buffer do nginx ou da CDN Envie X-Accel-Buffering: no

Se o res.php devolve o token e o navegador segue mudo, o defeito está na entrega.

Perguntas frequentes

Ainda preciso do res.php?

Como plano B, sim. Guarde o task_id e faça uma consulta única se o callback não chegar em 120 s.

Que valor chega pelo stream?

O campo code do callback: g-recaptcha-response no reCAPTCHA e cf-turnstile-response no Turnstile. O front-end insere esse valor no campo do formulário.

O SSE funciona atrás da Cloudflare?

Funciona, mas a CDN pode fazer buffer da resposta. Envie X-Accel-Buffering: no ou ative o modo de streaming na rota.

Manter o stream aberto consome threads do plano?

Não. A thread é ocupada pela tarefa em resolução, não pela conexão com o navegador.

E se o usuário fechar a aba antes do callback?

A conexão morre e o finally tira o cliente do registro. Guarde o token no Redis com TTL curto para a próxima conexão.

Próximos passos

Ligue o pingback ao seu stream e meça a diferença em staging: crie sua chave de API da CaptchaAI e teste hoje.

Guias relacionados:

Os comentários estão desativados para este artigo.