> ## 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

* [`waitForCaptchaSolve`](#waitforcaptchasolve) — Wait for a CAPTCHA to be solved
* [`withWaitForCaptchaSolve`](#withwaitforcaptchasolve) — Execute a callback then wait for CAPTCHA solve
* [`onCaptchaEvent`](#oncaptchaevent) — Register a callback for CAPTCHA updates
* [`onceCaptchaEvent`](#oncecaptchaevent) — Register a one-time callback for a CAPTCHA update
* [`removeCaptchaEventListener`](#removecaptchaeventlistener) — Remove a previously registered callback

## Function reference

### waitForCaptchaSolve

```typescript theme={null}
import { waitForCaptchaSolve } from '@intuned/runtime';
import type { Page } from 'playwright';

export declare function waitForCaptchaSolve(
  page: Page,
  options?: {
    timeoutInMs?: number;
    settleDurationMs?: number;
  }
): Promise<void>;
```

Wait for a CAPTCHA to be solved.

Listens for CAPTCHA updates and resolves when all CAPTCHAs are solved or operation times out. Used when a CAPTCHA appears and you need to block until it's solved.

<Tip>
  Use `waitForCaptchaSolve` when the page is already loaded and the challenge is
  already visible. This direct form does not wait for `networkidle` before it
  starts checking solve status. If a navigation, submit, or click may trigger the
  challenge, prefer [`withWaitForCaptchaSolve`](#withwaitforcaptchasolve).
</Tip>

<br />

**Parameters**

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

<ParamField path="options" type="object" optional>
  Configuration options for the wait behavior.
</ParamField>

<ParamField path="options.timeoutInMs" type="number" default="10000" optional>
  Maximum wait time in milliseconds. Throws `TimeoutError` if exceeded.
</ParamField>

<ParamField path="options.settleDurationMs" type="number" default="5000" optional>
  Wait time in milliseconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
</ParamField>

<br />

**Returns**

Returns `Promise<void>` when solved or settle period elapses.

<br />

**Raises**

* `TimeoutError` — Thrown when `timeoutInMs` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Thrown when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `Error` — Thrown when listeners cannot be attached or the page context is invalid.

### withWaitForCaptchaSolve

```typescript theme={null}
import { withWaitForCaptchaSolve } from '@intuned/runtime';
import type { Page } from 'playwright';

export declare function withWaitForCaptchaSolve<T>(
  callback: (page: Page) => Promise<T>,
  options: {
    page: Page;
    timeoutInMs?: number;
    settleDurationMs?: number;
    waitForNetworkSettled?: boolean;
  }
): Promise<T>;
```

Execute a callback then wait for CAPTCHA to be solved.

Execute the provided callback function, then listen for CAPTCHA updates and resolve when all CAPTCHAs are solved or operation times out. Useful when you need to execute a method that triggers a CAPTCHA and must block until it's solved.

<br />

**Parameters**

<ParamField path="callback" type="(page: Page) => Promise<T>" required>
  Function to execute before waiting for solve. Receives the page object and can return a value.
</ParamField>

<ParamField path="options" type="object" required>
  Configuration options for the wait behavior.
</ParamField>

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

<ParamField path="options.timeoutInMs" type="number" default="10000" optional>
  Maximum wait time in milliseconds. Throws `TimeoutError` if exceeded.
</ParamField>

<ParamField path="options.settleDurationMs" type="number" default="5000" optional>
  Wait time in milliseconds before checking if CAPTCHAs appeared. Resets when a CAPTCHA is detected during the period.
</ParamField>

<ParamField path="options.waitForNetworkSettled" type="boolean" default="true" optional>
  Whether to wait for network idle before checking solve status.
</ParamField>

<br />

**Returns**

Returns `Promise<T>` with the result of the callback function.

<br />

**Raises**

* `TimeoutError` — Thrown when `timeoutInMs` elapses while CAPTCHAs are still being solved.
* `CaptchaSolveError` — Thrown when the CAPTCHA solver fails. Contains a [`CaptchaError`](#captchaerror) with the error code and details.
* `Error` — Thrown if something unexpected happens.

**Examples**

<CodeGroup>
  ```typescript Wait after navigating to a page with CAPTCHA theme={null}
  import { waitForCaptchaSolve } from '@intuned/runtime';
  import { goToUrl } from '@intuned/browser';
  import type { Page } from 'playwright';

  export async function automation(page: Page, params: any) {
    await goToUrl({ page, url: "https://www.google.com/recaptcha/api2/demo" });
    await waitForCaptchaSolve(page, {
      timeoutInMs: 120000,
      settleDurationMs: 5000
    });
  }
  ```

  ```typescript Navigate or click, then wait for CAPTCHA solve theme={null}
  import { withWaitForCaptchaSolve } from '@intuned/runtime';
  import { goToUrl } from '@intuned/browser';
  import type { Page } from 'playwright';

  export async function automation(page: Page, params: any) {
    const data = await withWaitForCaptchaSolve(
      async (page) => {
        await goToUrl({ page, url: "https://www.google.com/recaptcha/api2/demo" });
        return await page.textContent('.success-message');
      },
      {
        page,
        timeoutInMs: 120000,
        settleDurationMs: 5000,
        waitForNetworkSettled: true
      }
    );

    console.log('Form submitted:', data);
  }
  ```
</CodeGroup>

### onCaptchaEvent

```typescript theme={null}
import { onCaptchaEvent } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function onCaptchaEvent(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

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>(captcha: <a href="#captcha">Captcha</a>) =&gt; Promise&lt;void&gt; | void</span>} required>
  Callback function that executes when the status is observed. Receives a `Captcha` object as its only argument.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function subscribes and resolves immediately. The callback fires each time a CAPTCHA with the specified status is observed.

<br />

**Raises**

* `RuntimeError` — Thrown when listeners cannot be attached or the page context is invalid.

**Example**

<CodeGroup>
  ```typescript Log CAPTCHA status changes theme={null}
  import { onCaptchaEvent, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleCaptchaEvent(captcha: Captcha): Promise<void> {
    console.log(`Captcha ${captcha.id} (tab=${captcha.tabId}) status=${captcha.status} retries=${captcha.retryCount}`);

    if (captcha.status === "solved") {
      console.log(`Solved after ${captcha.retryCount} retries`);
    } else if (captcha.status === "error") {
      console.log(`Solve error: ${captcha.error}`);
    }
  }

  await onCaptchaEvent(page, 'solved', handleCaptchaEvent);
  ```
</CodeGroup>

### onceCaptchaEvent

```typescript theme={null}
import { onceCaptchaEvent } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function onceCaptchaEvent(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

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>(captcha: <a href="#captcha">Captcha</a>) =&gt; Promise&lt;void&gt; | void</span>} required>
  Callback function that executes when the status is observed. Receives a `Captcha` object as its only argument.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function subscribes and resolves immediately. The callback fires at most once.

<br />

**Raises**

* `RuntimeError` — Thrown when listeners cannot be attached or the page context is invalid.

**Example**

<CodeGroup>
  ```typescript One-time notification on solve theme={null}
  import { onceCaptchaEvent, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleSolveOnce(captcha: Captcha): Promise<void> {
    console.log(`One-time notify: captcha ${captcha.id} status=${captcha.status} retries=${captcha.retryCount}`);
  }

  await onceCaptchaEvent(page, 'solved', handleSolveOnce);
  ```
</CodeGroup>

### removeCaptchaEventListener

```typescript theme={null}
import { removeCaptchaEventListener } from '@intuned/runtime';
import type { Page } from 'playwright';
import type { Captcha, CaptchaStatus } from '@intuned/runtime';

export declare function removeCaptchaEventListener(
  page: Page,
  status: CaptchaStatus,
  f: (captcha: Captcha) => Promise<void> | void
): Promise<void>;
```

Remove a previously registered CAPTCHA callback.

Unsubscribe a callback registered using `onCaptchaEvent` or `onceCaptchaEvent`. 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="(captcha: Captcha) => any" required>
  The callback function that was originally registered. Must be the same function reference.
</ParamField>

<br />

**Returns**

Returns `Promise<void>`. The function unsubscribes the callback and resolves immediately.

<br />

**Raises**

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

**Example**

<CodeGroup>
  ```typescript Subscribe and unsubscribe theme={null}
  import { onCaptchaEvent, removeCaptchaEventListener, type Captcha } from '@intuned/runtime';
  import type { Page } from 'playwright';

  async function handleCaptchaEvent(captcha: Captcha): Promise<void> {
    console.log(`Captcha status: ${captcha.status}`);
  }

  // Subscribe to CAPTCHA updates
  await onCaptchaEvent(page, 'solved', handleCaptchaEvent);

  // Later, unsubscribe
  await removeCaptchaEventListener(page, 'solved', handleCaptchaEvent);
  ```
</CodeGroup>

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

## Best practices

* Use `withWaitForCaptchaSolve` when a navigation, submit, or click may trigger the challenge.
* Use `waitForCaptchaSolve` only after the page has already settled and the CAPTCHA is already present.
* Set timeout values high enough for real challenges. `60_000` to `120_000` ms is a safer default than `10_000` ms for production flows.
* Adjust the settle duration for the wait before checking status. It resets when CAPTCHAs are detected.
* Leave `waitForNetworkSettled` enabled unless you have a reason to skip it.
* Subscribe to CAPTCHA updates using `onCaptchaEvent` or `onceCaptchaEvent` for telemetry and monitoring.
* Store callback function references if you need to unsubscribe later.

## Type reference

### `Captcha`

```typescript theme={null}
import type { Captcha, CaptchaStatus, CaptchaError } from '@intuned/runtime';

export type Captcha = {
  id: string;
  tabId: number;
  type: string;
  status: CaptchaStatus;
  retryCount?: number;
  error?: CaptchaError;
};
```

**Properties**

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

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

<ParamField body="type" type="string" 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="retryCount" type="number" optional>
  Number of solve attempts made.
</ParamField>

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

### `CaptchaStatus`

```typescript theme={null}
import type { CaptchaStatus } from '@intuned/runtime';

export type CaptchaStatus = "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`

```typescript theme={null}
import type { CaptchaError, CaptchaErrorCode } from '@intuned/runtime';

export type CaptchaError = {
  code: CaptchaErrorCode;
  error?: any;
};
```

**Properties**

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

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

### `CaptchaErrorCode`

```typescript theme={null}
import type { CaptchaErrorCode } from '@intuned/runtime';

export type CaptchaErrorCode =
  | "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.
