Use Cases

Solução CAPTCHA para teste de endpoint de API em formulários da Web

Rodar um navegador no CI só para clicar em um reCAPTCHA é caro e, na maioria dos testes de endpoint, desnecessário. Para validar como o back-end trata o token — aceita, rejeita, expira — basta chamar a API de resolução e enviar a requisição direto ao endpoint.

Este guia monta esse fluxo em Python: uma classe resolve o token (reCAPTCHA v2, v3, Cloudflare Turnstile) e outra testa o endpoint com token válido, inválido e ausente — sem navegador.


Quando faz sentido testar assim

Se a pergunta é sobre o comportamento do back-end — não sobre o widget do CAPTCHA em si — o teste de endpoint responde mais rápido do que qualquer suíte com navegador.

  • Validação de back-end: o servidor aceita token válido e rejeita inválido ou ausente?
  • Teste de carga: a partir de qual frequência o endpoint passa a responder 429?
  • Integração: a API de envio funciona no pipeline de CI/CD, sem navegador?
  • Resposta a erro: token ausente ou expirado gera a mensagem certa?

Como funciona o fluxo

O fluxo tem quatro etapas independentes:

┌──────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Solve    │────▶│ Build      │────▶│ POST to      │────▶│ Validate     │
│ CAPTCHA  │     │ Request    │     │ Endpoint     │     │ Response     │
│ (API)    │     │ Payload    │     │              │     │              │
└──────────┘     └────────────┘     └──────────────┘     └──────────────┘
  1. Solve: peça o token à API de resolução.
  2. Build: monte o payload com o token no campo certo.
  3. POST: envie a requisição ao endpoint real.
  4. Validate: confira status code e corpo da resposta.

O navegador só entra em cena se o formulário depender de JavaScript para montar a requisição.

Para validar o back-end, a chamada HTTP direta já resolve — é isso que as duas classes abaixo fazem.


Construindo o testador

Duas peças resolvem o problema.

Uma classe pega o token via API; a outra usa esse token para bater no endpoint e decidir se a resposta veio como esperado.

Classe para obter o token CAPTCHA

Envia o desafio a in.php.

Faz polling em res.php até o token ficar pronto.

import time
import requests


class TokenProvider:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    def get_recaptcha_token(self, sitekey, pageurl, version="v2"):
        params = {
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
        }
        if version == "v3":
            params["version"] = "v3"
            params["action"] = "submit"
            params["score_qa"] = "0.9"
        return self._solve(params, initial_wait=15 if version == "v3" else 10)

    def get_turnstile_token(self, sitekey, pageurl):
        return self._solve({
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": pageurl,
        })

    def _solve(self, params, initial_wait=10):
        params["key"] = self.api_key
        params["json"] = 1
        resp = requests.post(f"{self.BASE}/in.php", data=params).json()
        if resp["status"] != 1:
            raise Exception(resp["request"])
        task_id = resp["request"]
        time.sleep(initial_wait)
        for _ in range(60):
            result = requests.get(
                f"{self.BASE}/res.php",
                params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
            ).json()
            if result["request"] == "CAPCHA_NOT_READY":
                time.sleep(5)
                continue
            if result["status"] == 1:
                return result["request"]
            raise Exception(result["request"])
        raise TimeoutError("Timed out")

Cobre reCAPTCHA v2, v3 (com action e score_qa) e Turnstile, com retentativa a cada 5 s, até 60 vezes.

Tipo captcha_type Campo do token Espera inicial
reCAPTCHA v2 recaptcha_v2 g-recaptcha-response 10 s
reCAPTCHA v3 recaptcha_v3 g-recaptcha-response 15 s
Turnstile turnstile cf-turnstile-response 10 s

Classe para testar o endpoint

Monta e envia a requisição com o token.

Depois roda três verificações por endpoint: sucesso, token inválido e token ausente.

import json
import time


class EndpointTester:
    def __init__(self, api_key):
        self.token_provider = TokenProvider(api_key)
        self.session = requests.Session()
        self.results = []

    def test_endpoint(self, config):
        """
        config: {
            "name": "test name",
            "url": "endpoint URL",
            "method": "POST",
            "captcha_type": "recaptcha_v2" | "recaptcha_v3" | "turnstile",
            "sitekey": "...",
            "pageurl": "...",
            "captcha_field": "g-recaptcha-response",
            "payload": { ... form data ... },
            "expected_status": 200,
            "expected_contains": "success",
        }
        """
        start = time.time()
        result = {"name": config["name"], "passed": False}

        try:
            # Get CAPTCHA token
            captcha_type = config.get("captcha_type", "recaptcha_v2")
            if captcha_type == "recaptcha_v2":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"]
                )
            elif captcha_type == "recaptcha_v3":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"], version="v3"
                )
            elif captcha_type == "turnstile":
                token = self.token_provider.get_turnstile_token(
                    config["sitekey"], config["pageurl"]
                )
            else:
                raise ValueError(f"Unknown captcha type: {captcha_type}")

            # Build payload
            payload = {**config.get("payload", {})}
            captcha_field = config.get("captcha_field", "g-recaptcha-response")
            payload[captcha_field] = token

            # Submit request
            method = config.get("method", "POST").upper()
            headers = config.get("headers", {})

            if config.get("json_body"):
                resp = self.session.request(
                    method, config["url"], json=payload, headers=headers
                )
            else:
                resp = self.session.request(
                    method, config["url"], data=payload, headers=headers
                )

            # Validate response
            result["status_code"] = resp.status_code
            result["response_length"] = len(resp.text)
            result["elapsed"] = round(time.time() - start, 2)

            # Check expected status
            expected_status = config.get("expected_status", 200)
            if resp.status_code != expected_status:
                result["error"] = f"Expected {expected_status}, got {resp.status_code}"
                self.results.append(result)
                return result

            # Check expected content
            expected = config.get("expected_contains")
            if expected and expected.lower() not in resp.text.lower():
                result["error"] = f"Response missing: '{expected}'"
                self.results.append(result)
                return result

            result["passed"] = True

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_invalid_token(self, config):
        """Test that endpoint rejects invalid CAPTCHA tokens."""
        invalid_config = {**config}
        invalid_config["name"] = f"{config['name']} (invalid token)"

        # Override with fake token
        payload = {**config.get("payload", {})}
        captcha_field = config.get("captcha_field", "g-recaptcha-response")
        payload[captcha_field] = "INVALID_TOKEN_12345"

        start = time.time()
        result = {"name": invalid_config["name"], "passed": False}

        try:
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            # Should reject — 4xx or error message
            if resp.status_code >= 400 or "error" in resp.text.lower() or "invalid" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted invalid CAPTCHA token"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_missing_token(self, config):
        """Test that endpoint rejects missing CAPTCHA token."""
        start = time.time()
        result = {"name": f"{config['name']} (missing token)", "passed": False}

        try:
            payload = config.get("payload", {})
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            if resp.status_code >= 400 or "captcha" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted request without CAPTCHA"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def run_suite(self, configs):
        """Run a full test suite against multiple endpoints."""
        for config in configs:
            self.test_endpoint(config)
            self.test_invalid_token(config)
            self.test_missing_token(config)
        return self.report()

    def report(self):
        passed = sum(1 for r in self.results if r["passed"])
        total = len(self.results)
        lines = [f"Endpoint Tests: {passed}/{total} passed", "=" * 50]
        for r in self.results:
            status = "PASS" if r["passed"] else "FAIL"
            elapsed = r.get("elapsed", "?")
            lines.append(f"  [{status}] {r['name']} ({elapsed}s)")
            if r.get("error"):
                lines.append(f"         Error: {r['error']}")
        return "\n".join(lines)

test_endpoint cobre o sucesso; test_invalid_token e test_missing_token confirmam a rejeição — o mínimo de uma suíte de regressão de CAPTCHA.


Exemplo de execução

Configure cada endpoint como um dicionário.

Rode a suíte inteira de uma vez:

tester = EndpointTester("YOUR_API_KEY")

configs = [
    {
        "name": "Contact form submission",
        "url": "https://example.com/api/contact",
        "captcha_type": "recaptcha_v2",
        "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "pageurl": "https://example.com/contact",
        "captcha_field": "g-recaptcha-response",
        "payload": {
            "name": "Test User",
            "email": "test@example.com",
            "message": "Automated test message",
        },
        "expected_status": 200,
        "expected_contains": "success",
    },
    {
        "name": "Newsletter signup",
        "url": "https://example.com/api/subscribe",
        "captcha_type": "turnstile",
        "sitekey": "0x4AAAA...",
        "pageurl": "https://example.com/newsletter",
        "captcha_field": "cf-turnstile-response",
        "payload": {
            "email": "test@example.com",
        },
        "expected_status": 200,
    },
]

report = tester.run_suite(configs)
print(report)

Saída:

Endpoint Tests: 5/6 passed
==================================================
  [PASS] Contact form submission (18.5s)
  [PASS] Contact form submission (invalid token) (0.3s)
  [PASS] Contact form submission (missing token) (0.2s)
  [PASS] Newsletter signup (14.2s)
  [FAIL] Newsletter signup (invalid token) (0.3s)
         Error: Endpoint accepted invalid CAPTCHA token
  [PASS] Newsletter signup (missing token) (0.2s)

O relatório mostra na hora qual verificação falhou.

Aqui, o endpoint de newsletter aceitou um token que deveria ser rejeitado — bug real de validação, do tipo que passa despercebido sem esse teste.

Cuidado com dados reais nos testes

O payload usa test@example.com, não um e-mail real — em formulários com dado pessoal, use sempre dados fictícios, o que evita conflito com a LGPD.

Se os workers do CI rodam fora do Brasil, rode a suíte perto (sa-east-1 da AWS) para não confundir latência de rede com tempo de resolução.

Dica: rode primeiro os testes de token ausente e inválido — são instantâneos e não dependem da CaptchaAI. Deixe o teste de token válido, mais lento, para o job que já espera chamar a API de resolução.


Problemas comuns e como corrigir

Sintoma Causa provável Como corrigir
Token válido rejeitado Expirou entre a resolução e o envio Diminua o intervalo entre resolver e enviar
Token inválido aceito Back-end não valida o CAPTCHA de fato Bug de segurança — o endpoint confia no cliente
Toda requisição retorna 403 Faltam cookies de sessão ou CSRF Inclua os cookies ou o cabeçalho CSRF
Endpoint em JSON rejeita o payload Content-Type errado Defina json_body: True na configuração

Perguntas frequentes

Preciso resolver um CAPTCHA real para testar o endpoint?

Depende. Para testar rejeição de token ausente ou inválido, não — basta enviar sem token real. Para o caminho de sucesso, sim: é preciso um token genuíno da CaptchaAI.

O token do reCAPTCHA serve para mais de uma requisição?

Não. Cada token vale para uma submissão e expira em minutos. Falha por token expirado costuma ser reaproveitamento ou demora entre resolver e enviar.

Dá para rodar essa suíte em um pipeline como o GitHub Actions?

Sim. O fluxo é só HTTP, roda em qualquer runner sem instalar navegador — guarde a chave de API como secret e chame a suíte no job de testes.

O reCAPTCHA v3 retorna score baixo no ambiente de teste — é normal?

Sim. O v3 pontua o comportamento de navegação, não só o desafio, então dados sintéticos tendem a pontuar pior do que tráfego real. Ajuste score_qa na chamada à API para simular o score que você quer validar, sem depender de tráfego de produção.

Testar o endpoint direto substitui o teste end-to-end no navegador?

Não totalmente. O teste de endpoint garante que o back-end valida o token; o E2E no navegador ainda confirma que o widget carrega e gera o token na prática.


Guias relacionados


Teste todos os seus endpoints protegidos por CAPTCHA — crie sua conta na CaptchaAI e comece agora.

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