Use Cases

CAPTCHA Solving for API Endpoint Testing in Web Forms

Test CAPTCHA-protected endpoints without a browser — solve CAPTCHAs via API and submit directly to backend endpoints.


When You Need This

  • Backend validation testing: Verify the server correctly validates CAPTCHA tokens
  • Load testing: Submit many requests to CAPTCHA-protected endpoints
  • Integration testing: Test form submission APIs in CI/CD
  • Error response testing: Verify proper error messages for invalid/expired tokens

Architecture

┌──────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Solve    │────▶│ Build      │────▶│ POST to      │────▶│ Validate     │
│ CAPTCHA  │     │ Request    │     │ Endpoint     │     │ Response     │
│ (API)    │     │ Payload    │     │              │     │              │
└──────────┘     └────────────┘     └──────────────┘     └──────────────┘

No browser needed for most endpoint tests.


Implementation

CAPTCHA Token Provider

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["min_score"] = "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")

Endpoint Tester

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)

Usage

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)

Output:

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)

Troubleshooting

Issue Cause Fix
Valid token rejected Token expired before submission Reduce delay between solve and submit
Invalid token accepted Backend not validating CAPTCHA File bug — security issue
403 on all requests Missing CSRF token or cookies Add session cookies or CSRF header
JSON endpoint rejects form data Wrong content type Set json_body: True in config

FAQ

Can I test endpoints without solving a real CAPTCHA?

For invalid/missing token tests, no CAPTCHA solving is needed — just submit without a token. For valid submission tests, you need a real token from CaptchaAI.

How do I test rate-limited endpoints?

Add a delay between requests and test with increasing frequency. Track when the endpoint starts returning 429 responses.

Should I test CAPTCHA validation in unit tests?

Mock the CAPTCHA validation in unit tests. Use this approach for integration and end-to-end tests where you need real CAPTCHA tokens.



Test every CAPTCHA-protected endpoint — use CaptchaAI.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.