Troubleshooting

CAPTCHA Acceptance Checks After Solving with CaptchaAI

A returned CAPTCHA token is not the finish line.

It is only one checkpoint.

For a production automation or QA workflow, the real question is not:

Did CaptchaAI return a token?

The real question is:

Did the protected workflow accept the token and complete the intended action?

Those are different outcomes.

If you do not separate them, your logs will say “CAPTCHA solved” while the form still fails, the account flow still stops, the test still times out, or the scraping job still receives a blocked response.

This guide shows how to add acceptance checks around CaptchaAI so your integration can prove what happened before solving, during solving, after token handoff, and after backend verification.

Why acceptance checks matter

Most CAPTCHA integrations start with a simple mental model:

  1. Find the sitekey.
  2. Send the task to CaptchaAI.
  3. Poll for the token.
  4. Put the token into the page.
  5. Submit.

That works for a demo.

It is incomplete for production.

In real workflows, a token can be returned and still fail because:

  • the sitekey and page URL were captured from the wrong page state
  • the token was applied in a different browser session
  • the wrong hidden field was updated
  • a callback was required but never called
  • cookies changed between challenge and submit
  • the backend rejected the submission
  • the token was reused
  • the automation retried an unsafe action and duplicated work

Acceptance checks prevent those failures from becoming invisible.

They tell you exactly where the workflow stopped.

The three success states you must separate

Do not use a single captcha_solved=true flag.

Use three separate states.

State Meaning Example success signal
Solver success CaptchaAI returned a token or answer. API response contains status: 1 and a token.
Handoff success Your code applied the token into the correct page/session path. g-recaptcha-response or cf-turnstile-response was updated, or callback was called.
Workflow acceptance The target application accepted the token and completed the next step. HTTP 200, redirect to success page, dashboard state changed, or expected DOM appears.

The highest-value log line is not solver success.

The highest-value log line is workflow acceptance.

Before solving: capture the acceptance contract

Before calling CaptchaAI, record what the workflow will later need to prove.

This is the acceptance contract.

Field Why it matters
CAPTCHA type Determines the method, response field, and handoff path.
Page URL Must match the page context where the CAPTCHA appears.
Sitekey Must belong to the active widget.
Session ID Proves solve and submit happened in the same browser or HTTP session.
Cookie fingerprint Detects session changes before submission.
User agent Helps diagnose backend mismatch.
Expected handoff field g-recaptcha-response, cf-turnstile-response, or another field.
Expected callback Required when the page uses JavaScript completion logic.
Expected acceptance signal Redirect, DOM change, API status, or known success message.

Without this contract, post-failure debugging becomes guesswork.

Acceptance contract example

{
  "captcha_type": "cloudflare_turnstile",
  "pageurl": "https://example.com/protected-form",
  "sitekey_source": "turnstile.render",
  "sitekey_present": true,
  "browser_session_id": "session-20260429-001",
  "cookie_fingerprint": "sha256:7e20f4...",
  "user_agent_family": "Chromium",
  "expected_handoff": "cf-turnstile-response",
  "expected_callback": null,
  "expected_acceptance": {
    "type": "url_contains",
    "value": "/success"
  }
}

This is not just logging for debugging. It is a safety mechanism.

It lets your worker decide whether a retry is useful, dangerous, or pointless.

During solving: record solver state separately

CaptchaAI token-based flows use the same general pattern:

  1. Submit the task to https://ocr.captchaai.com/in.php.
  2. Receive a task ID.
  3. Wait before polling.
  4. Poll https://ocr.captchaai.com/res.php until a token is ready or a terminal error occurs.

For reCAPTCHA v2, the API method is userrecaptcha, with googlekey and pageurl.

For Cloudflare Turnstile, the API method is turnstile, with sitekey and pageurl.

Track those calls as solver state only.

Do not mark the workflow complete when the token arrives.

Python: solve and return structured solver state

import time
import requests
from dataclasses import dataclass


API_KEY = "YOUR_API_KEY"
IN_URL = "https://ocr.captchaai.com/in.php"
RES_URL = "https://ocr.captchaai.com/res.php"


@dataclass
class SolverResult:
    status: str
    task_id: str | None
    token: str | None
    error: str | None
    solve_seconds: float


class CaptchaAIError(Exception):
    pass


def solve_token_captcha(method: str, params: dict, timeout_seconds: int = 120) -> SolverResult:
    if not API_KEY or API_KEY == "YOUR_API_KEY":
        raise ValueError("Set your CaptchaAI API key before running this script.")

    started = time.time()

    payload = {
        "key": API_KEY,
        "method": method,
        "json": 1,
        **params,
    }

    submit = requests.post(IN_URL, data=payload, timeout=30)
    submit.raise_for_status()
    submit_data = submit.json()

    if submit_data.get("status") != 1:
        return SolverResult(
            status="submit_failed",
            task_id=None,
            token=None,
            error=submit_data.get("request"),
            solve_seconds=time.time() - started,
        )

    task_id = submit_data["request"]
    deadline = time.time() + timeout_seconds

    time.sleep(15)

    while time.time() < deadline:
        result = requests.get(
            RES_URL,
            params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            },
            timeout=30,
        )
        result.raise_for_status()
        result_data = result.json()

        if result_data.get("status") == 1:
            return SolverResult(
                status="token_returned",
                task_id=task_id,
                token=result_data["request"],
                error=None,
                solve_seconds=time.time() - started,
            )

        message = result_data.get("request")

        if message == "CAPCHA_NOT_READY":
            time.sleep(5)
            continue

        return SolverResult(
            status="solve_failed",
            task_id=task_id,
            token=None,
            error=message,
            solve_seconds=time.time() - started,
        )

    return SolverResult(
        status="timeout",
        task_id=task_id,
        token=None,
        error="SOLVE_TIMEOUT",
        solve_seconds=time.time() - started,
    )

Example usage for reCAPTCHA v2:

result = solve_token_captcha(
    method="userrecaptcha",
    params={
        "googlekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "pageurl": "https://example.com/login",
    },
)

print(result)

Example usage for Turnstile:

result = solve_token_captcha(
    method="turnstile",
    params={
        "sitekey": "0x4AAAAAA_example_sitekey",
        "pageurl": "https://example.com/protected-form",
    },
)

print(result)

Expected output:

SolverResult(status='token_returned', task_id='123456789', token='TOKEN_VALUE', error=None, solve_seconds=23.4)

This output proves only one thing: CaptchaAI returned a token.

It does not prove the application accepted it.

After solving: perform the handoff check

The handoff check verifies that your code applied the token to the page path the site expects.

For reCAPTCHA v2, the common response field is:

g-recaptcha-response

For Cloudflare Turnstile, the common response field is:

cf-turnstile-response

Some pages use callbacks instead of relying only on hidden fields.

Your code should report how the token was applied.

Browser-side handoff function

function applyCaptchaToken({ captchaType, token, callbackName = null }) {
  if (!token) {
    throw new Error("Missing CAPTCHA token.");
  }

  const fieldSelectors = {
    recaptcha_v2: '[name="g-recaptcha-response"]',
    cloudflare_turnstile: '[name="cf-turnstile-response"]',
  };

  const selector = fieldSelectors[captchaType];

  if (selector) {
    const field = document.querySelector(selector);

    if (field) {
      field.value = token;
      field.innerHTML = token;
      field.dispatchEvent(new Event("input", { bubbles: true }));
      field.dispatchEvent(new Event("change", { bubbles: true }));

      return {
        handoff_status: "applied",
        handoff_method: selector,
        callback_called: false,
      };
    }
  }

  if (callbackName) {
    const callback = callbackName
      .split(".")
      .reduce((current, key) => current && current[key], window);

    if (typeof callback === "function") {
      callback(token);

      return {
        handoff_status: "applied",
        handoff_method: "callback",
        callback_called: true,
      };
    }
  }

  throw new Error("No valid CAPTCHA response field or callback was available.");
}

This function returns a handoff result instead of silently mutating the DOM.

That makes the next step measurable.

Node.js + Playwright: solve, hand off, and verify acceptance

This example shows the full pattern:

  1. capture the context
  2. solve with CaptchaAI
  3. apply the token
  4. submit the form
  5. verify the workflow acceptance signal
const { chromium } = require("playwright");
const axios = require("axios");

const API_KEY = "YOUR_API_KEY";
const IN_URL = "https://ocr.captchaai.com/in.php";
const RES_URL = "https://ocr.captchaai.com/res.php";

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveCaptchaAI(method, params, timeoutMs = 120000) {
  if (!API_KEY || API_KEY === "YOUR_API_KEY") {
    throw new Error("Set your CaptchaAI API key before running this script.");
  }

  const started = Date.now();

  const submit = await axios.post(
    IN_URL,
    new URLSearchParams({
      key: API_KEY,
      method,
      json: "1",
      ...params,
    }),
    { timeout: 30000 }
  );

  if (submit.data.status !== 1) {
    return {
      solver_status: "submit_failed",
      task_id: null,
      token: null,
      error: submit.data.request,
      solve_ms: Date.now() - started,
    };
  }

  const taskId = submit.data.request;
  const deadline = Date.now() + timeoutMs;

  await sleep(15000);

  while (Date.now() < deadline) {
    const result = await axios.get(RES_URL, {
      params: {
        key: API_KEY,
        action: "get",
        id: taskId,
        json: 1,
      },
      timeout: 30000,
    });

    if (result.data.status === 1) {
      return {
        solver_status: "token_returned",
        task_id: taskId,
        token: result.data.request,
        error: null,
        solve_ms: Date.now() - started,
      };
    }

    if (result.data.request === "CAPCHA_NOT_READY") {
      await sleep(5000);
      continue;
    }

    return {
      solver_status: "solve_failed",
      task_id: taskId,
      token: null,
      error: result.data.request,
      solve_ms: Date.now() - started,
    };
  }

  return {
    solver_status: "timeout",
    task_id: taskId,
    token: null,
    error: "SOLVE_TIMEOUT",
    solve_ms: Date.now() - started,
  };
}

async function run() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();

  await page.goto("https://example.com/protected-form", {
    waitUntil: "networkidle",
  });

  const contextBeforeSolve = await page.evaluate(() => {
    const recaptcha = document.querySelector(".g-recaptcha");
    const turnstile = document.querySelector(".cf-turnstile");

    if (turnstile) {
      return {
        captcha_type: "cloudflare_turnstile",
        method: "turnstile",
        sitekey: turnstile.getAttribute("data-sitekey"),
        pageurl: window.location.href,
        expected_field: "cf-turnstile-response",
        expected_acceptance: {
          type: "url_contains",
          value: "/success",
        },
      };
    }

    if (recaptcha) {
      return {
        captcha_type: "recaptcha_v2",
        method: "userrecaptcha",
        googlekey: recaptcha.getAttribute("data-sitekey"),
        pageurl: window.location.href,
        expected_field: "g-recaptcha-response",
        expected_acceptance: {
          type: "url_contains",
          value: "/success",
        },
      };
    }

    return null;
  });

  if (!contextBeforeSolve) {
    throw new Error("No supported CAPTCHA widget detected.");
  }

  const params =
    contextBeforeSolve.captcha_type === "cloudflare_turnstile"
      ? {
          sitekey: contextBeforeSolve.sitekey,
          pageurl: contextBeforeSolve.pageurl,
        }
      : {
          googlekey: contextBeforeSolve.googlekey,
          pageurl: contextBeforeSolve.pageurl,
        };

  const solverResult = await solveCaptchaAI(contextBeforeSolve.method, params);

  if (solverResult.solver_status !== "token_returned") {
    console.log({
      phase: "solver",
      context: contextBeforeSolve,
      solverResult,
      accepted: false,
    });

    await browser.close();
    return;
  }

  const handoffResult = await page.evaluate(({ captchaType, token }) => {
    const selectors = {
      recaptcha_v2: '[name="g-recaptcha-response"]',
      cloudflare_turnstile: '[name="cf-turnstile-response"]',
    };

    const selector = selectors[captchaType];
    const field = document.querySelector(selector);

    if (!field) {
      throw new Error(`Missing response field: ${selector}`);
    }

    field.value = token;
    field.innerHTML = token;
    field.dispatchEvent(new Event("input", { bubbles: true }));
    field.dispatchEvent(new Event("change", { bubbles: true }));

    return {
      handoff_status: "applied",
      handoff_method: selector,
    };
  }, {
    captchaType: contextBeforeSolve.captcha_type,
    token: solverResult.token,
  });

  await page.click('button[type="submit"]');

  const accepted = await page.waitForURL("**/success", { timeout: 15000 })
    .then(() => true)
    .catch(() => false);

  const finalLog = {
    captcha_type: contextBeforeSolve.captcha_type,
    solver_status: solverResult.solver_status,
    task_id: solverResult.task_id,
    solve_ms: solverResult.solve_ms,
    handoff_status: handoffResult.handoff_status,
    handoff_method: handoffResult.handoff_method,
    final_url: page.url(),
    accepted,
  };

  console.log(JSON.stringify(finalLog, null, 2));

  await browser.close();
}

run().catch((error) => {
  console.error(error);
  process.exit(1);
});

Expected accepted output:

{
  "captcha_type": "cloudflare_turnstile",
  "solver_status": "token_returned",
  "task_id": "123456789",
  "solve_ms": 23618,
  "handoff_status": "applied",
  "handoff_method": "[name=\"cf-turnstile-response\"]",
  "final_url": "https://example.com/success",
  "accepted": true
}

Expected rejected output:

{
  "captcha_type": "cloudflare_turnstile",
  "solver_status": "token_returned",
  "task_id": "123456789",
  "solve_ms": 24110,
  "handoff_status": "applied",
  "handoff_method": "[name=\"cf-turnstile-response\"]",
  "final_url": "https://example.com/protected-form",
  "accepted": false
}

The second result is not a CaptchaAI outage by default.

It means the solver phase succeeded and the failure happened after token return.

Acceptance signals by workflow type

Different workflows need different acceptance checks.

Workflow Good acceptance signal Weak signal to avoid
Login Redirect to dashboard or authenticated API response Button click completed
Signup Account created, email step reached, or expected confirmation appears Token field has a value
Search form Results page loaded with expected query Form submit event fired
Checkout QA Order review page reached in test environment CAPTCHA token returned
Internal admin workflow Expected entity was created or updated Browser did not crash
Data extraction Expected records returned with non-blocked status HTTP request finished

The acceptance signal should match the business or QA outcome, not the CAPTCHA step.

Retry rules after acceptance checks

Acceptance checks also tell you whether retrying is safe.

Failure phase Retry? Reason
Submit failed with auth or balance error No Fix account or key first.
Submit failed with bad parameters No Retrying repeats the same invalid request.
Polling returned CAPCHA_NOT_READY Yes Continue polling within timeout.
Solver returned transient server error Yes, limited Retry with backoff.
Token returned but handoff field missing No Fix page detection or callback logic.
Token handed off but backend rejected Maybe Retry only after confirming no duplicate action risk.
Form action may create records, accounts, or orders No blind retry Use idempotency key or operator review.

Blind retries are one of the easiest ways to waste balance and create duplicate workflow side effects.

Idempotency for post-solve actions

Some actions are safe to retry. Some are not.

Safe retry examples:

  • loading a search page
  • checking a public status page
  • running a read-only QA assertion
  • polling CaptchaAI while task is not ready

Risky retry examples:

  • account creation
  • checkout progression
  • booking submission
  • contact form submission
  • record creation in an admin system

For risky actions, add an idempotency key before the protected submit step.

Example:

{
  "workflow_id": "signup-test-20260429-001",
  "attempt": 1,
  "idempotency_key": "signup-test-20260429-001-submit"
}

Store the key with your workflow state. If acceptance is unclear, do not blindly repeat the protected action. Check the backend or manual QA signal first.

Metrics that matter

Track these metrics separately:

Metric Why it matters
Solve success rate Measures CaptchaAI API task completion.
Handoff success rate Measures your DOM or callback application logic.
Acceptance rate Measures whether the protected workflow completed.
Solver latency p50/p95 Helps size timeouts and queues.
Post-token rejection rate Catches session and handoff bugs.
Retry rate by phase Shows whether failures are real or self-inflicted.
Cost per accepted workflow Better than cost per returned token.

The strongest business metric is:

cost per accepted workflow

Not:

cost per token returned

A low token cost is not useful if downstream acceptance is poor.

Troubleshooting matrix

Symptom Failure phase Likely cause Fix
ERROR_ZERO_BALANCE Solver submit Account has insufficient balance Top up and alert before queue starts
ERROR_WRONG_USER_KEY Solver submit API key is wrong or malformed Re-copy key and rotate secret
ERROR_BAD_PARAMETERS Solver submit Required CAPTCHA fields are missing Validate sitekey/pageurl before calling API
CAPCHA_NOT_READY forever Solver polling Timeout too short, task slow, or polling loop bug Poll every 5 seconds with a 120-second cap
Token returned, field missing Handoff Wrong CAPTCHA type or dynamic page state Re-detect widget and expected response field
Token returned, callback missing Handoff Callback was not captured before render Inject callback capture before widget loads
Token applied, form still disabled Handoff Front-end state did not update Dispatch input/change or call callback
Token applied, backend rejects Acceptance Session, cookies, URL, or token context mismatch Keep same browser context and verify page URL
Workflow duplicated after retry Post-acceptance Unsafe retry after unclear outcome Add idempotency and backend state checks

What not to do

Do not:

  • mark a job complete when CaptchaAI returns a token
  • retry bad parameters without changing inputs
  • apply a token in a different browser session
  • ignore callback-based CAPTCHA implementations
  • measure only solver success rate
  • hide post-token rejection under generic timeout errors
  • retry account creation or booking flows without idempotency
  • treat frontend field injection as proof of backend acceptance

These mistakes make the system look healthy while the real workflow is failing.

FAQ

Is a returned token proof that CaptchaAI solved the CAPTCHA?

It proves that CaptchaAI returned a result for the submitted task. It does not prove the protected application accepted the token. You still need handoff and workflow acceptance checks.

What is the best acceptance check?

The best acceptance check is the business outcome of the workflow: dashboard reached, account step completed, record created, results page loaded, or expected backend status returned.

Should I retry when a token is rejected?

Only after identifying the failure phase. If the token was applied in the wrong session or the callback was missing, retrying the same solve does not fix the problem. If the target action is not idempotent, do not retry blindly.

How do I know whether the failure is CaptchaAI or my integration?

Separate the logs. If CaptchaAI returns status: 1 and a token, the solver phase succeeded. If the page rejects the token afterward, inspect handoff path, same-session continuity, cookies, callback execution, and backend acceptance.

What should I measure in production?

Measure solve success, handoff success, workflow acceptance, post-token rejection rate, retry rate by phase, and cost per accepted workflow.

Next step: Add acceptance checks to your CaptchaAI workflow

If your current integration logs only “token returned,” add one more layer: record the handoff method, submit the protected action, and verify the actual workflow acceptance signal.

That is the difference between a CAPTCHA solve demo and a production-ready CaptchaAI integration.

Add acceptance checks to your CaptchaAI workflow

Comments are disabled for this article.

Related Posts

Tutorials Python ThreadPoolExecutor for CAPTCHA Solving Parallelism
Use Python's Thread Pool Executor for concurrent CAPTCHA solving — run multiple Captcha AI requests in parallel without asyncio complexity.

Use Python's Thread Pool Executor for concurrent CAPTCHA solving — run multiple Captcha AI requests in paralle...

Jan 15, 2026
Tutorials Discord Webhook Alerts for CAPTCHA Pipeline Status
Send CAPTCHA pipeline alerts to Discord — webhook integration for balance warnings, error spikes, queue status, and daily summary reports with Captcha AI.

Send CAPTCHA pipeline alerts to Discord — webhook integration for balance warnings, error spikes, queue status...

Apr 09, 2026