Core conceptsBeta

CAPTCHAs

The platform notices a CAPTCHA that is waiting for a person, tells you, and pauses its own automation until someone solves it. It never solves CAPTCHAs itself.

Some pages ask for a CAPTCHA: a check that a person is there. When a page in a session shows one that needs a person, Boxline notices it and tells you. Agent runs and plain-English steps then wait until someone has solved it in the live view.

What happens

  1. A page in the session shows a CAPTCHA that needs a person.
  2. The platform notices it within a few seconds. The session’s attention shows it, and a captcha event with state "detected" is logged.
  3. Agent runs pause and plain-English steps wait. Your own Playwright or Puppeteer code is not paused: it decides for itself what to do.
  4. A person opens the session’s live view and solves it: you, a teammate, or one of your own users.
  5. The platform sees that it is gone. attention goes back to null, a "cleared" event is logged, and paused work continues by itself.

If nobody solves it in time (5 minutes for agent runs, 4 for steps), they fail with a clear error. The session itself keeps running until its own timeout, and is billed as usual while it waits.

Which CAPTCHAs are recognised

kindWhat it is
recaptchaGoogle reCAPTCHA: the checkbox, or its picture challenge.
hcaptchahCaptcha.
turnstileA Cloudflare Turnstile widget on a page.
cloudflareCloudflare’s full-page “Just a moment…” check.
datadomeA DataDome challenge.
arkoseAn Arkose Labs puzzle.
humanThe press-and-hold button from HUMAN (PerimeterX).

When a CAPTCHA counts as waiting for a person

  • A widget is visible on the page and not answered yet, or the whole page is a challenge (Cloudflare’s “Just a moment…”, HUMAN’s press-and-hold).
  • It is seen twice, about 2 seconds apart. A check that passes by itself within a couple of seconds, like most Cloudflare Turnstile checks, is never reported.
  • Invisible widgets are not reported (for example reCAPTCHA v3, or invisible reCAPTCHA and hCaptcha until they show a challenge). They need nothing from a person.
  • It is cleared when it has been answered, when it is gone, or when the page has moved on, again seen twice.

The platform looks after every page load and whenever a challenge appears, then every 2 seconds while one is open. It only reads the page; it never touches the widget. When several tabs have one, they are reported one at a time: the next one after the first is cleared.

The captcha option

Choose what agent runs and steps do when a CAPTCHA waits for a person. Set it when you create the session, and change it at any time with PATCH/v1/sessions/:id.

captchaWhat happens
"ask" (default)Agent runs pause and plain-English steps wait until a person has solved it, for up to 5 minutes (steps: 4).
"ignore"Agent runs and steps carry on as if nothing happened. attention and the captcha events still report it.
TypeScript
import { Boxline } from "@boxline/sdk";

const client = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URL
const session = await client.sessions.create({ browser: true, captcha: "ignore" });

// Later: wait for a person again
await session.update({ captcha: "ask" });

Any other value gets 400 invalid_request. There is no “solve” value.

See it on the session

While a CAPTCHA waits for a person, the session object has an attention field. Otherwise it is null.

Session (excerpt)
"captcha": "ask",
"attention": {
  "type": "captcha",
  "kind": "recaptcha",
  "url": "https://shop.example.com/login",
  "tabId": "…",
  "since": "2026-09-29T12:00:04.000Z"
}
FieldDescription
typeAlways "captcha".
kindWhich CAPTCHA (see the table above).
urlThe page that shows it.
tabIdThe tab it is in.
sinceWhen it was first reported (ISO time).

Events

Each CAPTCHA logs two session events of type captcha: one when it starts waiting for a person (data.state: "detected", level: "warn") and one when it is gone ("cleared", with data.waitedMs: how long it waited).

captcha events
{
  "seq": 41,
  "at": "2026-09-29T12:00:04.000Z",
  "type": "captcha",
  "level": "warn",
  "text": "reCAPTCHA on shop.example.com",
  "url": "https://shop.example.com/login",
  "tabId": "…",
  "data": { "kind": "recaptcha", "state": "detected" }
}
{
  "seq": 57,
  "at": "2026-09-29T12:00:52.000Z",
  "type": "captcha",
  "text": "reCAPTCHA cleared on shop.example.com",
  "url": "https://shop.example.com/login",
  "tabId": "…",
  "data": { "kind": "recaptcha", "state": "cleared", "waitedMs": 48000 }
}

Read them with types=captcha, or follow them live on /v1/sessions/:id/events/stream:

Shell
curl "https://api.staging.boxline.dev/v1/sessions/$SESSION_ID/events?types=captcha" \
  -H "x-api-key: $BOXLINE_API_KEY"

Agent runs

With captcha: "ask", an agent run checks for a CAPTCHA before each of its steps. When one is waiting, the run pauses:

GET /v1/agent/runs/:id (excerpt)
{
  "status": "paused",
  "handover": {
    "by": "captcha",
    "reason": "reCAPTCHA on shop.example.com: solve it in the live view and the agent continues"
  }
}
  • The run’s event stream (/v1/agent/runs/:id/events) sends {"type": "status", "status": "paused", "by": "captcha", "reason": "…"}.
  • Once a person has solved it, the run continues by itself (status goes back to "running"). The agent is told that a person solved a CAPTCHA, and looks at the page again before it goes on.
  • You can also hand back yourself with POST /v1/agent/runs/:id/handback. If the CAPTCHA is still waiting, the run pauses again.
  • If nobody solves it within 5 minutes, the run fails with status: "failed" and an error that starts with “nobody solved the CAPTCHA within 5 minutes”.

A run started without sessionId creates its session with captcha: "ask". To run an agent with "ignore", create the session yourself and pass its sessionId. With "ignore" the agent sees the CAPTCHA like any other page, and may still ask for help itself (handover.by: "agent").

This example sends someone the live view link when a run needs a person:

TypeScript
const { id, sessionId } = await client.agent.run({
  task: "Sign in to shop.example.com and download this month's invoice",
});
const session = await client.sessions.get(sessionId);

let notified = false;
for (;;) {
  const run = await client.agent.get(id);
  if (run.status !== "running" && run.status !== "paused") {
    console.log(run.status, run.result ?? run.error);
    break;
  }
  const needsPerson = run.handover?.by === "captcha";
  if (needsPerson && !notified) {
    // notifyOperator is your own function: chat, email, a ticket...
    await notifyOperator(`${run.handover!.reason}: ${session.liveUrl}`);
  }
  notified = needsPerson;
  await new Promise((r) => setTimeout(r, 2000));
}

Plain-English steps

With captcha: "ask", a plain-English step (session.step("click Sign in")) waits while a CAPTCHA waits for a person, for up to 4 minutes, then runs as usual.

If it is still there after 4 minutes, the step fails with the code captcha_timeout. The action result has ok: false, the code, and an error that names the CAPTCHA and the site:

POST /v1/sessions/:id/actions (response)
{
  "results": [
    {
      "ok": false,
      "action": "step",
      "code": "captcha_timeout",
      "error": "reCAPTCHA on shop.example.com is still waiting for a person after 240 s: solve it in the live view, then run the step again",
      "ms": …
    }
  ]
}

The SDKs raise it as a BoxlineError with status 409 and code captcha_timeout, so you can tell it apart from other failures:

TypeScript
try {
  await session.step("click Sign in");
} catch (err) {
  if (!(err instanceof BoxlineError) || err.code !== "captcha_timeout") throw err;
  // Nobody solved the CAPTCHA within 4 minutes. Tell someone, wait, then try again.
  await notifyOperator(`A CAPTCHA is waiting: ${session.liveUrl}`);
  await session.waitForHuman({ timeoutMs: 10 * 60_000 });
  await session.step("click Sign in");
}

A step that waits keeps its request open for up to 4 minutes, which stays under the 5-minute limit of common HTTP clients such as Node's fetch (the Python SDK allows 7 minutes for requests with a step). When you expect a CAPTCHA, calling waitForHuman before the step avoids the long request.

Your own Playwright or Puppeteer code

The platform does not pause code that drives the browser over CDP. Your script decides what to do. The SDKs have two helpers:

HelperWhat it does
session.waitForHuman({ timeoutMs }) (TypeScript), session.wait_for_human(timeout=300) (Python)Returns once no CAPTCHA is waiting for a person (someone solved it, or the page moved on). Returns at once if none is waiting. After the timeout (default 5 minutes) it throws a BoxlineError with code captcha_timeout.
session.onCaptcha(handler) (TypeScript)Calls handler with { state, kind, url, event } on every "detected" and "cleared" event. It checks every 2 seconds and stops by itself when the session ends. The function it returns stops it sooner.

The platform needs a few seconds to report a new CAPTCHA, so calling waitForHuman right after page.goto can return too early. Call it when the page does not look the way you expect:

TypeScript
import { Boxline, BoxlineError } from "@boxline/sdk";
import { chromium } from "playwright";

const client = new Boxline();
const session = await client.sessions.create({ browser: true, keepAlive: true });

// Tell someone as soon as a CAPTCHA needs a person (notifyOperator is your own function).
const stop = session.onCaptcha(async ({ state, kind, url }) => {
  if (state === "detected") await notifyOperator(`${kind} on ${url}: ${session.liveUrl}`);
});

try {
  const browser = await chromium.connectOverCDP(session.connectUrl!);
  const page = browser.contexts()[0].pages()[0];
  await page.goto("https://shop.example.com/login");

  const email = page.getByLabel("Email");
  try {
    await email.waitFor({ timeout: 10_000 });
  } catch {
    // The form did not show up: maybe a CAPTCHA is waiting for a person.
    await session.waitForHuman({ timeoutMs: 5 * 60_000 });
  }
  await email.fill("me@example.com");
} catch (err) {
  if (err instanceof BoxlineError && err.code === "captcha_timeout") {
    console.error("Nobody solved the CAPTCHA in time");
  }
  throw err;
} finally {
  stop();
  await session.release();
}

The Python SDK has no onCaptcha. To be told about every CAPTCHA, read the events yourself:

Python
import time

after = 0
while session.refresh().status == "RUNNING":
    batch = session.events(types=["captcha"], after=after)
    for event in batch["data"]:
        if event["data"]["state"] == "detected":
            notify_operator(f"{event['text']}: {session.live_url}")
    after = batch["nextAfter"]
    time.sleep(2)

Let your own users solve it

When your product works on your users’ own accounts, the right person to answer a CAPTCHA is often that user. Send them the session’s liveUrl: they see the browser, solve the CAPTCHA with their mouse and keyboard, and your code carries on when waitForHuman returns or the "cleared" event arrives.

  • Open it for them in a new tab or window, from a page of your product they are signed in to. Get a fresh one with GET /v1/sessions/:id/live.
  • The live view can be framed (in an <iframe>) only by Boxline’s own apps, such as the console. Embedding it on your own site needs embed origins set for your project, which are not available yet.

Fetch, screenshot, PDF and crawl

One-shot requests cannot wait for a person. They return the page as it is, and tell you when a CAPTCHA was waiting on it:

APIWhere to look
POST /v1/fetchcaptcha in the response: a kind, or null.
POST /v1/screenshot, /v1/pdfThe x-page-captcha response header: a kind, or empty.
POST /v1/crawlcaptcha on each page in GET /v1/crawl/:id.
TypeScript
const page = await client.fetch("https://shop.example.com/prices", { format: "markdown" });
if (page.captcha) {
  // The content is probably the challenge, not the page you wanted.
  console.log(`${page.captcha} on ${page.finalUrl}`);
}

// A crawl: which pages had a CAPTCHA waiting?
const job = await client.crawl.start({ url: "https://shop.example.com", maxPages: 50 });
const done = await client.crawl.wait(job.id);
const blocked = done.data.filter((p) => p.captcha).map((p) => `${p.captcha} on ${p.url}`);

When a page you need is behind a CAPTCHA, open it in a session instead, so a person can solve it there, or leave that page out.

Meet fewer CAPTCHAs

Sites show CAPTCHAs to visitors they do not recognise or that behave unlike people. These habits make them rarer, without trying to trick anyone:

  • Stay signed in. Start sessions from a context so the account’s cookies come along, and save it back with persist: true. A known, signed-in visitor is asked less often than a new one.
  • Keep one session for a whole task instead of many short ones.
  • Use a steady address near the account’s usual place. A sticky residential IP in the country, state or city where the account usually signs in helps: sites often ask for a check when an account suddenly signs in from somewhere new. Do not switch IPs in the middle of a login.
  • Keep the browser and the address in one story. With a proxy in another country, add "browser": { "mode": "realistic" } so the clock, language and Accept-Language match it. See Browser mode. It does not spoof fingerprints or hide that the session is automated.
  • Slow down. Load fewer pages per minute, wait for pages to finish, and do not open the same page over and over. The crawl API follows robots.txt and its crawl delay for you.
  • Use the site’s own API when it has one.
  • Respect the site’s terms. If a site does not allow automated access, do not automate it.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.