Docs
Dashboard
Guides / SessionsBeta

CAPTCHAs

The platform notices a CAPTCHA that is waiting for a person, tells you, and pauses its own automation until someone solves it. On paid plans a session can opt in to automatic solving first.

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.

On paid plans, a session can also opt in to automatic solving. The platform then tries to solve the CAPTCHA first, and asks a person only when it cannot.

What happens by default

  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 isTried by "solve"
recaptchaGoogle reCAPTCHA: the checkbox, or its picture challenge.Yes (reCAPTCHA v2)
hcaptchahCaptcha.Yes
turnstileA Cloudflare Turnstile widget on a page.Yes
cloudflareCloudflare’s full-page “Just a moment…” check.No: a person is asked
datadomeA DataDome challenge.No: a person is asked
arkoseAn Arkose Labs puzzle.No: a person is asked
humanThe press-and-hold button from HUMAN (PerimeterX).No: a person is asked

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. With "ask" and "ignore" it only reads the page and never touches the widget. With "solve" it also fills in the answer (see Automatic solving). 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 happens when a CAPTCHA waits for a person. Set it when you create the session (or in the session of an agent run), and change it at any time with PATCH/v1/sessions/:id, in Update a session.

captchaWhat happens
"ask" (default, unless your project default says otherwise)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.
"solve"The platform tries to solve it first, and nobody is asked meanwhile. If it cannot or may not, a person is asked, exactly like "ask". Opt-in, on plans that include automatic CAPTCHA solving. See Automatic solving.
curl -X POST https://api.boxline.dev/v1/sessions \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"browser": true, "captcha": "ignore"}'curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"captcha": "ask"}'

Any other value gets 400 invalid_request. On a plan without automatic CAPTCHA solving, "solve" gets 402 feature_not_in_plan: on create, on update and in a run’s session. See Plans and features.

The project default

A session or agent run that does not set captcha (or sets it to null) gets your project’s captchaDefault: "ask" unless you change it. In the console, Settings → General, “When a CAPTCHA appears”: “Solve automatically” sets it to "solve".

  • An explicit captcha always wins, and is checked against your plan as before.
  • Sessions already running keep the setting they started with.
  • Crawls, and fetch, screenshot, PDF and extract, do not use it: they cannot wait for a person or solve, so they report the CAPTCHA’s kind. See below.
  • If your plan loses automatic solving, a "solve" default stays saved but new sessions get "ask": captchaDefaultEffective shows which.
curl -X PUT https://api.boxline.dev/v1/project/settings \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"captchaDefault": "solve"}'

See Project settings.

Automatic solving

With captcha: "solve", the platform tries to solve a CAPTCHA before anyone is asked. You turn it on per session. It is off by default.

Only plans that include automatic CAPTCHA solving can use it. By default that is every paid plan, and not Free.

What it tries

Three kinds: reCAPTCHA v2 (the checkbox and its picture challenge), hCaptcha and Cloudflare Turnstile. The others always go to a person (see the table above).

What happens

  1. The CAPTCHA is noticed as usual, and a "detected" event is logged.
  2. If it may be solved, attention.state is "solving". Nobody needs to act. Agent runs and plain-English steps wait.
  3. The platform sends the page’s address and the widget’s public site key to a third-party solving service. It fills the answer into the widget, and logs "solving" and "solved" events.
  4. Once the page shows the widget answered, the CAPTCHA is cleared: attention goes back to null, a "cleared" event is logged, and paused work continues by itself.

Filling in the answer does not submit the page’s form. The agent, your step or your own code carries on from there, as after a person has solved it.

Each CAPTCHA is tried once. If it cannot or may not be solved, a person is asked, exactly like "ask": attention.state becomes "waiting" and attention.reason says why.

When a person is asked instead

WhenEvent data.stateEvent data.reason
The kind cannot be solved: Cloudflare’s “Just a moment…” page, DataDome, Arkose or HUMAN.solve_refusedunsupported
The site is a government or military site (.gov, .mil and national forms such as .gov.uk or .gouv.fr), or one the platform never solves on.solve_refusedblocked_site
Your plan does not include automatic solving, or this month’s allowance is used up.solve_refusedplan_limit
Solving is switched off on the platform right now.solve_refuseddisabled
The solving service could not solve it, or the widget could not be read.solve_failednone

Refusals are not billed. Their attention.reason says the same in words, for example “this kind of CAPTCHA cannot be solved automatically”.

How long work waits

  • Agent runs wait while it is being solved, with a handover.reason that says so. Their 5 minutes for a person start only if it becomes a person’s turn.
  • Plain-English steps wait up to 4 minutes in all, solving included.
  • waitForHuman / wait_for_human wait through solving too. They return once no CAPTCHA is waiting, however it was solved.

Allowance and billing

  • Every attempt that reached the solving service is billed, solved or not. Refusals are free.
  • The price is per 1,000 attempts: captchaSolving.per1000Attempts in GET /v1/pricing. It may change during the beta, so read it from there or the pricing page.
  • Your plan sets a monthly allowance: project.limits.captchaSolvesPerMonth in GET /v1/auth/me (0 means none, null means no cap). It counts attempts that reached the service in the calendar month (UTC), in all the projects of your organization together. It is a ceiling, not free attempts.
  • When the allowance is used up, CAPTCHAs go to a person (plan_limit) until the 1st. Sessions keep working.
  • GET /v1/project/usage reports captchaSolves, and its total costUsd includes them.
GET /v1/project/usage (excerpt)
"captchaSolves": {  "solved": 41,  "failed": 3,  "refused": 7,  "costUsd": …}

Use it safely

  • Turn it on only for the sessions that need it, and leave the others on "ask". You can switch a running session in Update a session. The change applies within about 3 seconds, and a CAPTCHA that is already waiting is tried then.
  • The page’s full address goes to the solving service, query string included. Do not use "solve" on pages whose address carries a secret or personal data, such as a sign-in link with a token in it.
  • Only the address and the widget’s public site key are sent. Your cookies, the page’s content and your API key are not.
  • Watch the solving events and captchaSolves in usage. If a script keeps meeting CAPTCHAs, slow it down instead of paying for more attempts (see Meet fewer CAPTCHAs).

Example

Check the plan, start a session with "solve", and tell a person only when it is their turn:

curl -X POST https://api.boxline.dev/v1/sessions \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"browser": true, "captcha": "solve"}'

See it on the session

While a CAPTCHA is open, 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",  "state": "waiting"}
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).
state"waiting": a person’s turn (always, with "ask" and "ignore"). "solving": it is being solved automatically, and nobody needs to act.
reasonOnly when automatic solving was refused or failed: why, in words.

With "solve", a CAPTCHA that could not be solved looks like this:

Session (excerpt)
"captcha": "solve","attention": {  "type": "captcha",  "kind": "human",  "url": "https://shop.example.com/login",  "tabId": "…",  "since": "2026-09-29T12:00:04.000Z",  "state": "waiting",  "reason": "this kind of CAPTCHA cannot be solved automatically"}

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).

With "solve", more events come in between:

data.stateWhen
"detected"A CAPTCHA started waiting (level: "warn").
"solving"The solving service is working on it.
"solved"The answer was filled in. "cleared" follows once the page shows it answered.
"solve_failed"The service could not solve it. A person is asked (level: "warn").
"solve_refused"It may not be solved. data.reason is unsupported, blocked_site, plan_limit or disabled (see the table above). A person is asked (level: "warn").
"cleared"It is gone, with data.waitedMs.
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.boxline.dev/v1/sessions/$SESSION_ID/events?types=captcha" \  -H "x-api-key: $BOXLINE_API_KEY"

Agent runs

With captcha: "ask" or "solve", 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/stream) sends {"type": "status", "status": "paused", "by": "captcha", "reason": "…"}.
  • With "solve", the reason first says it is being solved automatically, and that you are asked only if that fails. If it fails, the reason changes to say it is a person’s turn, and the stream sends a new status event.
  • Once it is solved, the run continues by itself (status goes back to "running"). The agent is told that a CAPTCHA was solved, and looks at the page again before it goes on.
  • You can also resume the run yourself with Resume a run (POST /v1/agent/runs/:id/resume). 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”. With "solve", the 5 minutes start only when it becomes a person’s turn.

Pass captcha in the run’s session to choose for the session it creates ("ask" by default; see the agent’s session). With sessionId, that session’s own option applies. 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:

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));}

With "solve", the run is also paused while the CAPTCHA is being solved. Before you notify someone, check that the session’s attention.state is "waiting".

Plain-English steps

With captcha: "ask" or "solve", a plain-English step (session.step("click Sign in")) waits while a CAPTCHA is open, for up to 4 minutes in all, then runs as usual. With "solve", the time spent solving counts toward the 4 minutes.

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/browser/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:

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. With "solve", the platform fills in the answer while your code runs, so wait for it before you submit the page’s form. 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 (it was solved, by a person or automatically, 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), session.on_captcha(handler) (Python)Calls handler with { state, kind, url, event } (in Python a dict with the same keys) on every "detected" and "cleared" event. It checks every 2 seconds and stops by itself when the session stops. The function it returns stops it sooner. For the solving states, read the events yourself.

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:

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.stop();}

In Python, on_captcha does the same. To be told about every CAPTCHA, including the solving states, read the events yourself:

Python
import time# Simple: be told when a CAPTCHA needs a person, and when it is gone.def on_change(change):    if change["state"] == "detected":        notify_operator(f"{change['kind']} on {change['url']}: {session.live_url}")stop = session.on_captcha(on_change)  # call stop() to stop watching# Or read every captcha event yourself:after = 0while 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.next_after    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 session returns a fresh one every time.
  • 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, and automatic solving is for sessions only. 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/crawlscaptcha on each page in GET /v1/crawls/:id.
curl -s -D - -o page.png -X POST https://api.boxline.dev/v1/screenshot \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"url": "https://shop.example.com/prices"}' | grep -i x-page-captcha

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 browser profile 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.