> ## Documentation Index
> Fetch the complete documentation index at: https://intunedhq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CAPTCHA helpers

When you enable [CAPTCHA solving](/main/02-features/stealth-mode-captcha-solving-proxies#captcha-solving) in your project, CAPTCHAs are solved automatically in the background. However, your automation may need to wait for a CAPTCHA to be solved before proceeding, or know when a CAPTCHA was solved.

These helpers let you wait for CAPTCHAs to be solved and react to status changes.

<Note>
  CAPTCHA is a general term for challenges that verify you're human. This includes reCAPTCHA, hCaptcha, Cloudflare Turnstile, and similar services.
</Note>

## Available helpers

* [`wait_for_captcha_solve`](#wait_for_captcha_solve) — Wait for a CAPTCHA to be solved
* [`on_captcha_event`](#on_captcha_event) — Register a callback for CAPTCHA updates
* [`once_captcha_event`](#once_captcha_event) — Register a one-time callback for a CAPTCHA update
* [`remove_captcha_event_listener`](#remove_captcha_event_listener) — Remove a previously registered callback

## Function reference

### wait\_for\_captcha\_solve

Wait for a CAPTCHA to be solved. Supports three usage patterns: callable, wrapper, and decorator.

**Note:** The decorator pattern is available in Python only. TypeScript uses direct call and wrapper patterns. In Python, the decorator can be used either as `@wait_for_captcha_solve` or `@wait_for_captcha_solve(...)`.

<Tip>
  Use the callable form only when the page is already loaded and the challenge is
  already visible. The callable form does not wait for `networkidle`. If a
  navigation, submit, or click may trigger the challenge, prefer the wrapper or
  decorator forms.
</Tip>

<Tabs>
  <Tab title="Callable">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    async def wait_for_captcha_solve(
        page: Page,
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
    ) -> None: ...
    ```

    This callable form supports both `wait_for_captcha_solve(page, ...)` and
    `wait_for_captcha_solve(page=page, ...)`. The wrapper form below is keyword-only
    because it requires both `page=` and `func=`.

    **Parameters**

    <ParamField path="page" type="Page" required>
      Playwright page object.
    </ParamField>

    <ParamField path="timeout_s" type="float" default="10.0" optional>
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField path="settle_period" type="float" default="5.0" optional>
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    **Returns**

    Returns `None` when solved or settle period elapses.
  </Tab>

  <Tab title="Wrapper">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    async def wait_for_captcha_solve(
        *,
        page: Page,
        func: Callable[..., Any],
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
        wait_for_network_settled: bool = True,
    ) -> Any: ...
    ```

    **Parameters**

    <ParamField path="page" type="Page" required>
      Playwright page object.
    </ParamField>

    <ParamField path="func" type="Callable[..., Any]" required>
      Function to execute before waiting for solve. Can be sync or async.
    </ParamField>

    <ParamField path="timeout_s" type="float" default="10.0" optional>
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField path="settle_period" type="float" default="5.0" optional>
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    <ParamField path="wait_for_network_settled" type="bool" default="True" optional>
      Whether to wait for network idle before starting the wait for solve operation.
    </ParamField>

    **Returns**

    Returns the result of `func`, or `None` if `func` has no return value.
  </Tab>

  <Tab title="Decorator">
    ```python theme={null}
    from intuned_runtime.captcha import wait_for_captcha_solve

    def wait_for_captcha_solve(
        timeout_s: float = 10.0,
        settle_period: float = 5.0,
        wait_for_network_settled: bool = True,
    ): ...
    ```

    **Parameters**

    <ParamField path="timeout_s" type="float" default="10.0" optional>
      Maximum wait time in seconds. Raises `TimeoutError` if exceeded.
    </ParamField>

    <ParamField path="settle_period" type="float" default="5.0" optional>
      Wait time in seconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
    </ParamField>

    <ParamField path="wait_for_network_settled" type="bool" default="True" optional>
      Whether to wait for network idle before starting the wait for solve operation.
    </ParamField>

    **Returns**

    Preserves the wrapped function's return type. The decorated function must accept a `Page` as its first argument.
  </Tab>
</Tabs>

<br />

**Raises**

* `TimeoutError` — Raised when `timeout_s` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Raised when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

<br />

**Examples**

<CodeGroup>
  ```python Wait after navigating to a page with CAPTCHA theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page
  from intuned_browser import go_to_url

  async def automation(page: Page, params, **_kwargs):
      await go_to_url(page=page, url="https://www.scrapingcourse.com/login/cf-turnstile")
      await wait_for_captcha_solve(page=page, timeout_s=120.0, settle_period=5.0)
  ```

  ```python Navigate or click, then wait for CAPTCHA solve theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page
  from intuned_browser import go_to_url

  async def automation(page: Page, params, **_kwargs):
      await wait_for_captcha_solve(
          page=page,
          func=lambda: go_to_url(page=page, url="https://www.scrapingcourse.com/login/cf-turnstile"),
          timeout_s=120.0,
          settle_period=5.0,
          wait_for_network_settled=True,
      )
  ```

  ```python Reusable decorator for form submission theme={null}
  from intuned_runtime.captcha import wait_for_captcha_solve
  from playwright.async_api import Page

  @wait_for_captcha_solve
  async def submit_and_wait(page: Page):
      await page.click('#submit')

  async def automation(page: Page, params, **_kwargs):
      await submit_and_wait(page)
  ```
</CodeGroup>

### on\_captcha\_event

```python theme={null}
from intuned_runtime.captcha import on_captcha_event, Captcha, CaptchaStatus
from playwright.async_api import Page

async def on_captcha_event(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Register a callback for CAPTCHA updates.

Subscribe to CAPTCHA updates on a page. The callback fires every time a CAPTCHA with the specified status is observed. The subscription remains active until the page or context is destroyed. Use this for monitoring, logging, or reacting to CAPTCHA status.

<br />

**Parameters**

<ParamField path="page" type="Page" required>
  Playwright page object.
</ParamField>

<ParamField path="status" type={<a href="#captchastatus">CaptchaStatus</a>} required>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField path="f" type={<span>Callable[[<a href="#captcha">Captcha</a>], Awaitable[None] | None]</span>} required>
  Callback function that executes when the status is observed. Receives a `Captcha` instance as its only argument.
</ParamField>

<br />

**Returns**

Returns `None`. The function subscribes and returns immediately. The callback fires each time a CAPTCHA with the specified status is observed.

<br />

**Raises**

* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

**Example**

<CodeGroup>
  ```python Log CAPTCHA status changes theme={null}
  from intuned_runtime.captcha import on_captcha_event, Captcha
  from playwright.async_api import Page


  async def handle_captcha_event(captcha: Captcha) -> None:
      print(f"Captcha {captcha.id} (tab={captcha.tab_id}) status={captcha.status} retries={captcha.retry_count}")

      if captcha.status == "solved":
          print(f"Solved after {captcha.retry_count} retries")
      elif captcha.status == "error":
          print(f"Solve error: {captcha.error}")


  await on_captcha_event(page, status='solved', f=handle_captcha_event)
  ```
</CodeGroup>

### once\_captcha\_event

```python theme={null}
from intuned_runtime.captcha import once_captcha_event, Captcha, CaptchaStatus
from playwright.async_api import Page

async def once_captcha_event(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Register a one-time callback for a CAPTCHA update.

Subscribe to CAPTCHA updates on a page. The callback fires once when a CAPTCHA with the specified status is observed, then automatically unsubscribes. Use this when you need to respond only to the next occurrence, such as recording when a CAPTCHA is solved or performing cleanup.

<br />

**Parameters**

<ParamField path="page" type="Page" required>
  Playwright page object.
</ParamField>

<ParamField path="status" type={<a href="#captchastatus">CaptchaStatus</a>} required>
  The CAPTCHA status to listen for.
</ParamField>

<ParamField path="f" type={<span>Callable[[<a href="#captcha">Captcha</a>], Awaitable[None] | None]</span>} required>
  Callback function that executes when the status is observed. Receives a `Captcha` instance as its only argument.
</ParamField>

<br />

**Returns**

Returns `None`. The function subscribes and returns immediately. The callback fires at most once.

<br />

**Raises**

* `RuntimeError` — Raised when the subscription cannot be created or the page context is invalid.

**Example**

<CodeGroup>
  ```python One-time notification on solve theme={null}
  from intuned_runtime.captcha import once_captcha_event, Captcha
  from playwright.async_api import Page


  async def handle_solve_once(captcha: Captcha) -> None:
      print(f"One-time notify: captcha {captcha.id} status={captcha.status} retries={captcha.retry_count}")


  await once_captcha_event(page, status='solved', f=handle_solve_once)
  ```
</CodeGroup>

### remove\_captcha\_event\_listener

```python theme={null}
from intuned_runtime.captcha import remove_captcha_event_listener, Captcha, CaptchaStatus
from playwright.async_api import Page
from typing import Callable, Awaitable

async def remove_captcha_event_listener(
    page: Page,
    status: CaptchaStatus,
    f: Callable[[Captcha], Awaitable[None] | None],
) -> None: ...
```

Remove a previously registered CAPTCHA callback.

Unsubscribe a callback registered using `on_captcha_event` or `once_captcha_event`. You must pass the same page, status, and callback function that were used to register the callback.

<br />

**Parameters**

<ParamField path="page" type="Page" required>
  Playwright page object.
</ParamField>

<ParamField path="status" type="CaptchaStatus" required>
  The CAPTCHA status that the listener was registered for.
</ParamField>

<ParamField path="f" type="Callable[[Captcha], Awaitable[None] | None]" required>
  The callback function that was originally registered. Must be the same function reference.
</ParamField>

<br />

**Returns**

Returns `None`. The function unsubscribes the callback and returns immediately.

<br />

**Raises**

* `RuntimeError` — Raised when the callback cannot be removed or the page context is invalid.

**Example**

<CodeGroup>
  ```python Subscribe and unsubscribe theme={null}
  from intuned_runtime.captcha import on_captcha_event, remove_captcha_event_listener, Captcha
  from playwright.async_api import Page


  async def handle_captcha_event(captcha: Captcha) -> None:
      print(f"Captcha status: {captcha.status}")


  # Subscribe to CAPTCHA updates
  await on_captcha_event(page, status='solved', f=handle_captcha_event)

  # Later, unsubscribe
  await remove_captcha_event_listener(page, status='solved', f=handle_captcha_event)
  ```
</CodeGroup>

**Note:** The callback function reference must match exactly. Anonymous functions cannot be removed. Callbacks registered with `once_captcha_event` are automatically removed after firing.

## Best practices

* Use the wrapper or decorator forms when a navigation, submit, or click may trigger the challenge.
* Use the callable form only after the page has already settled and the CAPTCHA is already present.
* Set timeout values high enough for real challenges. `60.0` to `120.0` seconds is a safer default than `10.0` seconds for production flows.
* Adjust the settle period for the wait before checking status. It resets when CAPTCHAs are detected.
* Leave `wait_for_network_settled` enabled for wrapper and decorator usage unless you have a reason to skip it.
* Subscribe to CAPTCHA updates using `on_captcha_event` or `once_captcha_event` for telemetry and monitoring.
* Store callback function references if you need to unsubscribe later.

## Type reference

### `Captcha`

```python theme={null}
from pydantic import BaseModel, Field
from intuned_runtime.captcha import Captcha, CaptchaStatus, CaptchaError

class Captcha(BaseModel):
    model_config = {
        "populate_by_name": True,
        "serialize_by_alias": True,
    }
    id: str
    tab_id: int = Field(alias="tabId", default=0)
    type: str
    status: CaptchaStatus
    retry_count: int = Field(alias="retryCount", default=0)
    error: CaptchaError | None = None
```

**Properties**

<ParamField body="id" type="str" required>
  Unique identifier for the CAPTCHA observation.
</ParamField>

<ParamField body="tab_id" type="int" required>
  Browser tab ID where the CAPTCHA was detected.
</ParamField>

<ParamField body="type" type="str" required>
  CAPTCHA provider type, such as `recaptcha`, `hcaptcha`, or `cloudflare`.
</ParamField>

<ParamField body="status" type={<a href="#captchastatus">CaptchaStatus</a>} required>
  Current solving state.
</ParamField>

<ParamField body="retry_count" type="int">
  Number of solve attempts made. Defaults to `0`.
</ParamField>

<ParamField body="error" type={<span><a href="#captchaerror">CaptchaError</a> | None</span>} optional>
  Error details when `status` is `error`, otherwise `None`.
</ParamField>

### `CaptchaStatus`

```python theme={null}
from typing import Literal
from intuned_runtime.captcha import CaptchaStatus

CaptchaStatus = Literal["attached", "solving", "solved", "error", "detached"]
```

**Values**

* **`attached`** — CAPTCHA element detected in the page. Use this to know when a challenge appears.
* **`solving`** — Solver is actively processing the CAPTCHA.
* **`solved`** — CAPTCHA solved successfully. Resume your workflow.
* **`error`** — Solver failed. Check the `error` field for details.
* **`detached`** — CAPTCHA element removed from the page. Treat as cancelled.

### `CaptchaError`

```python theme={null}
from pydantic import BaseModel
from typing import Any
from intuned_runtime.captcha import CaptchaError, CaptchaErrorCode

class CaptchaError(BaseModel):
    code: CaptchaErrorCode
    error: Any | None = None
```

**Properties**

<ParamField body="code" type={<a href="#captchaerrorcode">CaptchaErrorCode</a>} required>
  Error code indicating the type of failure.
</ParamField>

<ParamField body="error" type="Any | None" optional>
  Additional error details, if available.
</ParamField>

### `CaptchaErrorCode`

```python theme={null}
from typing import Literal
from intuned_runtime.captcha import CaptchaErrorCode

CaptchaErrorCode = Literal[
    "HIT_LIMIT",
    "MAX_RETRIES",
    "UNEXPECTED_ERROR",
    "UNEXPECTED_SERVER_RESPONSE"
]
```

**Values**

* **`HIT_LIMIT`** — Reached billing limits for CAPTCHA solves. See [Plans and billing](/main/05-references/plans-and-billing) for details on limits and upgrading.
* **`MAX_RETRIES`** — Exceeded maximum retry attempts specified in [`settings.maxRetries`](/main/02-features/stealth-mode-captcha-solving-proxies#settings-reference).
* **`UNEXPECTED_ERROR`** — An unexpected error occurred while solving. This is a solver error and not related to your automation.
* **`UNEXPECTED_SERVER_RESPONSE`** — The solver received an unexpected response. This is a solver error and not related to your automation.
