Docs
Dashboard
SDKsBeta

Node SDK

The TypeScript SDK for Node 18 and later: install it, create a client, and every method it has.

The Node SDK (@boxline/sdk) has a typed method for every endpoint of the HTTP API. It retries safely, sends idempotency keys for you, pages through lists, and throws typed errors. It is written in TypeScript and runs on Node 18 and later.

Install

Shell
npm install @boxline/sdk playwright-core

playwright-core is only needed to drive the browser with Playwright. The playwright package works too.

Open source (MIT licence): github.com/Boxline-dev/sdk-node. The package on npm: @boxline/sdk.

This page describes version 3.0.0.

Create a client

The client reads BOXLINE_API_KEY and BOXLINE_API_URL from the environment. Without BOXLINE_API_URL it uses the public API, like the CLI. For an API on your own computer, set it to http://localhost:8080.

Shell
export BOXLINE_API_KEY="your-api-key"export BOXLINE_API_URL="https://api.boxline.dev"
TypeScript
import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URL// Or set the options yourself.const custom = new Boxline({  apiKey: process.env.BOXLINE_API_KEY,  baseUrl: "https://api.boxline.dev",  maxRetries: 3,  timeoutMs: 60_000,});
OptionDefaultWhat it does
apiKeyBOXLINE_API_KEYYour API key.
baseUrlBOXLINE_API_URL, then the public APIThe API’s address, https://api.boxline.dev.
maxRetries2How often a safe request is tried again. See Retries.
timeoutMs120000The time limit of one request, in milliseconds.
headersnoneHeaders sent with every request.
fetchthe global fetchYour own fetch function, for tests or tracing.
credentialsnoneFor browser apps on Boxline’s own sites that use the console’s login cookie instead of a key.

Sessions and Playwright

A session is an isolated machine with a browser (the default), a shell, or both, and a /workspace disk they share. Connect Playwright to a browser session’s connectUrl, or drive the same browser with the actions API, one HTTP call per list of actions.

TypeScript
import { Boxline } from "@boxline/sdk";import { chromium } from "playwright-core";const bx = new Boxline();const session = await bx.sessions.create({ timeout: 300 });try {  // A signed URL: treat it like a password.  const browser = await chromium.connectOverCDP(session.connectUrl!);  const page = browser.contexts()[0]!.pages()[0]!;  await page.goto("https://example.com");  console.log(await page.title());  // The actions API drives the same browser without Playwright.  const { title, content } = await session.content("markdown");  console.log(title, content.length);} finally {  await session.stop(); // billing stops here}

session.liveUrl is a page where a person can watch and take over. session.stop() saves the browser and files and stops billing; session.resume() brings the session back as it was, and session.delete() deletes it for good. A call on a stopped session does not resume it: it throws SessionNotRunningError.

connectUrl, liveUrl and terminalUrl are signed: anyone who has one can use the session, so treat each like a password. bx.sessions.get(id) returns fresh ones every time, and session.refresh() reads them again into the object you hold. A URL is null where the session has no browser (connectUrl, liveUrl) or no shell (terminalUrl).

Every method that works on one session is on bx.sessions and takes the session id first, such as bx.sessions.stop(id). A Session object has the same methods without the id, such as session.stop(). session.data holds the session as the API last returned it.

Browser, shell, or both

browser is on by default and shell is off. Pick the kind when you create the session. You cannot change it later. See Configuring sessions.

TypeScript
const browserOnly = await bx.sessions.create({});                               // the defaultconst shellOnly = await bx.sessions.create({ browser: false, shell: true });const both = await bx.sessions.create({ shell: true });

A browser call (actions, computer, runScript, exportCookies, and the session.goto() family) on a shell-only session throws BrowserDisabledError. A shell call (exec, restartShell) on a session without a shell throws ShellDisabledError.

Change a running session

session.update() is the one call for every change to a running session. It takes the field names and shapes sessions.create has, and the session object holds the answer afterwards.

TypeScript
const session = await bx.sessions.create({ timeout: 300, proxy: { type: "residential", country: "DE" } });await session.update({ timeout: 900 });         // its length in seconds from its last start; expiresAt movesawait session.update({ idleTimeout: 120 });     // end it after 2 minutes without activityawait session.update({ rotateProxy: true });    // a new proxy IP now; open browser connections reconnect (400 without a proxy)await session.update({ proxy: null });          // remove the proxyawait session.update({ blockAds: true, cookieBanners: "reject", captcha: "ask" });await session.update({ rotateUrls: true });     // every earlier signed URL stops workingconsole.log(session.liveUrl);                   // the new one: share it only with people who may use it
  • timeout is at least 60 seconds from now and at most your plan’s maximum (a longer value throws PlanLimitError). A longer session bills for longer: set idleTimeout so a session whose client crashed ends by itself.
  • rotateUrls: true is for a URL that leaked, or for a person who should lose access. Every earlier connectUrl, liveUrl and terminalUrl stops working at once, so a Playwright connection made with the old one is closed. The call returns the new URLs.
  • Fields fixed when the session is created (browser or shell on or off, viewport, profile, recordSession, setup, extensions, env, credentials) throw NotUpdatableError. The message names the field.
  • A session that is not running throws SessionNotRunningError.

See Update a session and the API reference.

Browser, shell and files together

One session has a browser, a shell and a disk they share. Browser downloads land in /workspace/downloads, the shell starts in /workspace, and the files API reads and writes it. This example downloads a report, adds it up with Python, and types the total into a form.

report.ts
import { Boxline } from "@boxline/sdk";import { chromium } from "playwright";const bx = new Boxline();const session = await bx.sessions.create({ shell: true, keepAlive: true });try {  // Browser: download a report. Downloads land in /workspace/downloads.  // (A demo page; on your own site, page.goto() your report page instead.)  const browser = await chromium.connectOverCDP(session.connectUrl!);  const context = browser.contexts()[0];  const page = context.pages()[0] ?? (await context.newPage());  await page.setContent(    `<a download="report.csv" href="data:text/csv,${encodeURIComponent("month,revenue\nJan,1200\nFeb,1850\nMar,2410")}">Download report</a>`,  );  await page.getByRole("link", { name: "Download report" }).click();  const csv = await session.files.waitFor("downloads/*.csv");  console.log("downloaded", csv.path, csv.size);  // Shell: add up the revenue column with Python, on the same machine.  const run = await session.exec(`mkdir -p output && python3 -c "import pandas as pd; print(pd.read_csv('downloads/report.csv')['revenue'].sum())" > output/total.txt`);  if (run.exitCode !== 0) throw new Error(run.stderr);  // Files: read the result.  const total = (await session.files.readText("output/total.txt")).trim(); // "5460"  // Actions: type it into the form and send it.  const results = await session.actions([    { action: "goto", url: "https://httpbin.org/forms/post" },    { action: "fill", selector: "input[name=custname]", value: total },    { action: "click", selector: "form button" },    { action: "content", format: "text" },  ]);  console.log(results.at(-1)?.value?.content); // what the form received  await browser.close();} finally {  await session.stop();}

The same mix works inside an agent run and inside a script. See Browser, shell and files.

Files, also after a session stops

Reading a file, listing a folder and downloading a folder work on a stopped session too, until it is deleted. No machine starts. Writing, deleting and waiting for a file need a running session.

TypeScript
import { writeFile } from "node:fs/promises";const tar = await session.files.archive("output");     // a folder as one .tar.gz (a Uint8Array)await writeFile("output.tar.gz", tar);await session.stop();console.log(await session.files.readText("output/total.txt")); // still readable while stoppedconst entries = await session.files.list("output");

A folder over 1 GiB or 100,000 entries throws ArchiveTooLargeError before any byte is sent. A stopped session that saved nothing throws NothingSavedError. See File uploads and downloads.

The page APIs

One call each. The page loads in a real browser inside a sandbox.

TypeScript
const page = await bx.fetch("https://example.com", { format: "markdown", links: true });const png = await bx.screenshot("https://example.com", { fullPage: true }); // Uint8Arrayconst pdf = await bx.pdf("https://example.com", { paper: "A4" });const { data } = await bx.extract<{ plans: { name: string; price: string }[] }>({  url: "https://example.com/pricing",  prompt: "The name and monthly price of every plan",});const started = await bx.crawl.start({ url: "https://example.com/docs", maxPages: 20 });const crawl = await bx.crawl.wait(started.id); // every page in crawl.data

A page that cannot load throws PageUnreachableError (502) or PageTimeoutError (504), and a PDF address NotAWebPageError (422) from fetch and extract. With several urls, extract lists a page that did not load in pages, with error, instead of throwing. See Web.

TypeScript
const { results, cached } = await bx.search({ query: "playwright connectOverCDP", limit: 5, recency: "year" });for (const r of results) console.log(r.title, r.url, r.publishedAt ?? "");// fetch: also open the top pages (true = 3, or 0 to 5) and get them as Markdown.const withPages = await bx.search({ query: "playwright connectOverCDP", limit: 3, fetch: 2 });console.log(withPages.results[0].content?.slice(0, 200), withPages.results[0].error);

The same search within an hour comes from the cache (cached: true) and is not counted. The query goes to Brave, a search provider: keep passwords and personal data out of it. See Search.

Mouse, keyboard and computer use

Coordinates are CSS pixels of the page (1280 by 720 by default). Everything acts on the page through the browser, never on the machine’s desktop, and moves in straight lines.

TypeScript
const s = await bx.sessions.create();await s.goto("https://example.com");await s.mouse.move(400, 300, { steps: 10 });await s.mouse.click(400, 300, { button: "right", count: 2, modifiers: ["Shift"] });await s.mouse.drag("#card", "#done-column", { steps: 20 });                          // selectors: the element's centreawait s.mouse.drag([{ x: 100, y: 100 }, { x: 200, y: 150 }, { x: 300, y: 100 }]); // or a pathawait s.mouse.down();await s.mouse.up();await s.hover("#menu");await s.keyboard.key("ControlOrMeta+A");console.log(await s.cursor());                                                     // { x, y }const shot = await s.screenshot({ maxWidth: 640, cursor: true });                  // with width, height, scale// Every result says what happened; a bare string is a plain-English step.const results = await s.actions([{ action: "goto", url: "https://example.com" }, "click More information"]);console.log(results.map((r) => r.text));// One action exactly as a computer-use model gave it, and the screen after it.const screen = await s.computer({ action: "left_click", coordinate: [512, 300] }, { maxWidth: 1024 });

A point outside the page throws OutOfViewportError. See Mouse and keyboard and Computer use, which has a loop for each provider. For the agent, bx.agent.run({ task, mode: "computer" }) works in computer mode.

An agent run with variables

The model sees %email% and %password%, never the values. A value is typed only where the model types text or picks an option. origins limits it to fields on those sites; use it for passwords.

TypeScript
const run = await bx.agent.run({  task: "Sign in to https://example.com with %email% and %password%, then open the billing page.",  variables: {    email: "ada@example.com",    password: { value: process.env.SITE_PASSWORD!, origins: ["https://example.com"] },  },});for await (const event of bx.agent.stream(run.id)) {  if (event.type === "thought") console.log("…", event.text);     // what it is about to do  if (event.type === "tool") console.log(event.name, event.input); // placeholders, never values  if (event.type === "done") console.log(event.status, event.result);}

bx.agent.wait(id) polls until the run ends. A run works in its own session. Describe it with session, which takes the fields sessions.create takes. A run in a session without a browser has no browser tools. See AI agent.

TypeScript
// A shell-only run: no browser, so the AI gets the shell and the files only.const run = await bx.agent.run({  task: "Convert every video in /workspace to MP4 with ffmpeg and list the new files",  session: { browser: false, shell: true },});const done = await bx.agent.wait(run.id);console.log(done.status, done.result);

session and sessionId together are refused. See the run’s session and shell-only runs.

Pause and resume a run

bx.agent.pause(id) lets the AI finish its step and wait (status paused). A person can then act in the session’s live view or terminal. bx.agent.resume(id) goes on: the same run, with the same id, and its steps and usage add up. Resume also carries on a run that stopped early, at a limit or after a server restart. Its resumable.until says how long that works.

TypeScript
const run = await bx.agent.run({ task: "Download the latest invoice from https://example.com/billing" });await bx.agent.pause(run.id, "I will sign in myself");     // the AI finishes its step, then waitsconst session = await bx.sessions.get(run.sessionId);console.log(session.liveUrl);                              // a signed link: share it only with that personawait bx.agent.resume(run.id, { note: "I signed in" });    // the same run goes onconst done = await bx.agent.wait(run.id);if (done.resumable) {  // It stopped at a limit. Give it more steps; the work so far is kept.  await bx.agent.resume(run.id, { maxSteps: 60, note: "The invoice page is already open" });}

A run that had variables needs their values again when it resumes after a limit. A run that is neither paused nor stopped early throws NotResumableError. See Pause and resume.

A script with useModel

runScript runs Playwright code inside the session, so it needs shell: true. In the script, page, context, browser, env, require, and the plain-English helpers step(), extract() and useModel() are ready to use.

TypeScript
const session = await bx.sessions.create({ shell: true });const { exitCode } = await session.runScript(  `  useModel("claude-haiku-5-5");                               // every later step() and extract() uses it  await page.goto("https://example.com/signup");  await step("type %email% into the email field");  await step("click Continue", { model: "claude-sonnet-5-5" }); // this call only  const plans = await extract("the plan names and prices");   // returns the data itself  console.log(JSON.stringify(plans));  `,  { env: { email: "ada@example.com" }, onData: (_stream, text) => process.stdout.write(text) },);console.log("exit code", exitCode);await session.stop();

See Scripts.

CAPTCHAs

By default (captcha: "ask"), Boxline notices a CAPTCHA that waits for a person and hands over. Agent runs and steps pause, session.data.attention says which CAPTCHA, and a person solves it in the live view. "ignore" carries on. "solve" (paid plans) tries to solve it first. Only automate sites you are allowed to.

TypeScript
const session = await bx.sessions.create({ captcha: "ask" });const stop = session.onCaptcha(({ state, kind, url }) => {  // Tell someone who may solve it; session.liveUrl is a signed link, so share it privately.  if (state === "detected") console.log(`${kind} on ${url}: solve it in the live view`);});await session.goto("https://example.com/signup");await session.waitForHuman({ timeoutMs: 5 * 60_000 }); // throws CaptchaTimeoutError if nobody solves itstop();

See CAPTCHAs.

Browser settings and extensions

Ad and tracker blocking and cookie-banner answers are on every plan. Set them when a session starts, or change them while it runs:

TypeScript
const s = await bx.sessions.create({ blockAds: true, cookieBanners: "reject" }); // "reject" is the defaultawait s.goto("https://example.com");await s.refresh();console.log(s.data.blockedRequests); // refused inside the machine, before any proxyawait s.update({ blockAds: false, cookieBanners: "off" });await bx.fetch("https://example.com", { blockAds: true }); // screenshot, pdf, extract, crawl.start and agent.run too

Chrome extensions (Manifest V3, Hobby plan and up): upload the zip once, then start sessions with it.

TypeScript
const ext = await bx.extensions.upload("./my-extension.zip"); // or its bytes, at most 10 MBconst withExt = await bx.sessions.create({ extensions: [ext.id] }); // loaded before the session is returnedfor await (const e of bx.extensions.list()) console.log(e.id, e.name, e.version, e.permissions);await bx.extensions.delete(ext.id);

An extension sees every page and every value typed in the session, agent variables too. Upload only extensions you trust. An agent run with variables in a session with extensions throws VariablesWithExtensionsError unless you pass allowWithExtensions: true. See Ad and cookie-banner blocking and Extensions.

Credentials, 2FA codes and signing in

Save a password once, and sign a session in with one call. When the site emails or texts its 2FA code, save the password with codeSource: "push" and send each code with credentials.pushCode while the run waits for it. With codeSource: "url" and a codeUrl, the platform asks your endpoint instead, and verifyWebhook checks each request.

TypeScript
import { CredentialCodeTimeoutError, CredentialLoginFailedError } from "@boxline/sdk";await bx.credentials.create({  name: "SHOP",  type: "password",  origins: ["https://shop.example.com"],  username: "ops@example.com",  password: process.env.SHOP_PASSWORD!,  codeSource: "push", // your system sends each code or sign-in link});const session = await bx.sessions.create();try {  // login waits as long as the server may (a code from your system included).  const page = await session.login("SHOP", { url: "https://shop.example.com/signin" });  console.log(page.url, page.title, page.runId);} catch (err) {  if (err instanceof CredentialLoginFailedError) console.log("could not sign in; run", err.runId); // also the timeout  else if (err instanceof CredentialCodeTimeoutError) console.log("no code arrived in time");  else throw err;}// In your webhook receiver, when credential.code_needed arrives (the run is waiting):await bx.credentials.pushCode("SHOP", { code: "123456" }); // what the site just emailed or texted

credentials.update(name, { rotateCodeUrlSecret: true }) makes a new signing secret for a codeUrl and returns it once. The old secret stops at once, so put the new one into your endpoint straight away. See Credentials.

Webhooks

TypeScript
import { verifyWebhook, WebhookSignatureError } from "@boxline/sdk";const endpoint = await bx.webhooks.create({ url: "https://example.com/webhooks/boxline", events: ["session.stopped"] });// endpoint.secret ("whsec_…") is shown only now: store it with your other secrets.// In your receiver: the raw body, the Boxline-Signature header, and the secret (or [new, old] after a rotation).try {  const event = verifyWebhook(body, header, process.env.BOXLINE_WEBHOOK_SECRET!); // 5-minute window  // drop event.id if you have handled it already, then answer 200} catch (err) {  if (err instanceof WebhookSignatureError) console.log(err.reason); // answer 400}for await (const d of bx.webhooks.deliveries(endpoint.id, { status: "failed" })) console.log(d.id, d.errorCode);await bx.webhooks.test(endpoint.id);

webhooks.update(id, { rotateSecret: true }) makes a new signing secret and returns it once. The old one keeps signing for 24 hours, so give verifyWebhook both while you switch. retryDelivery(id, deliveryId), update and delete do the rest, and webhookSignatureHeader(secret, body) signs bodies for your receiver’s tests. See Rotate a signing secret and Webhooks.

Project settings

TypeScript
const settings = await bx.project.settings(); // { captchaDefault, captchaDefaultEffective, trajectories, updatedVia, … }console.log(settings.trajectories);            // { enabled, noticeSeenAt, decidedBy }await bx.project.setSettings({ captchaDefault: "ignore" });      // for new sessions and runs without captchaawait bx.project.setSettings({ trajectories: { enabled: false } }); // the Trajectories program, for this project

See Project settings and Trajectories.

Usage

bx.usage() is the one call for what the project used and spent. It covers this month so far, or the period you pass. running and concurrencyLimit show how close you are to your plan’s limit on sessions at once.

TypeScript
const u = await bx.usage({ from: "2026-10-01", to: "2026-10-07" });console.log(`${u.running} of ${u.concurrencyLimit} sessions running`);console.log(u.sessions, u.agentRuns, u.modelCostUsd, u.ownKeyModelCostUsd, u.costUsd);for (const day of u.byDay) console.log(day.date, day.sessions, day.agentRuns, day.costUsd);

See Pricing.

Lists and pages

Every list takes limit and after. Await it for one page. Iterate it with for await for every item: the SDK follows next for you.

TypeScript
const page = await bx.sessions.list({ status: ["RUNNING", "STOPPED"], limit: 50 });console.log(page.data.length, page.total, page.next);for await (const s of bx.sessions.list({ status: "RUNNING", kind: "shell" })) console.log(s.id); // shell-only sessionsfor await (const e of bx.sessions.events("SESSION_ID", { types: ["console", "error"] })) console.log(e.text);for await (const p of page.iterPages()) console.log(p.data.length);

The lists are sessions.list, sessions.events, sessions.pages, profiles.list, apiKeys.list, agent.list and crawl.list. A crawl’s pages come from crawl.get(id, { after }), or all of them from for await (const p of bx.crawl.pages(id)). See Pagination.

Errors

Every failure throws a BoxlineError with status, code (stable, in snake_case), message, requestId and clientRequestId. Quote the requestId when you ask for support. Catch a subclass to handle one case:

ClassStatusWhen
RateLimitError429rate_limited or concurrency_limit. retryAfter has the seconds to wait.
FeatureNotInPlanError402feature_not_in_plan: your plan does not include it.
ProjectSuspendedError403project_suspended: the message says why.
IdempotencyMismatchError422idempotency_mismatch: the key was used for another request.
IdempotencyInProgressError409idempotency_in_progress: the first request with the key is still running. Retried by itself.
InvalidCursorError400invalid_cursor: after is not a next value of that list.
CaptchaTimeoutError409 or 408captcha_timeout: a CAPTCHA kept waiting for a person (409 from a step, 408 from waitForHuman).
PageUnreachableError502page_unreachable: the page could not be loaded (fetch, screenshot, pdf, extract).
PageTimeoutError504page_timeout: the page did not finish loading within timeoutMs.
NotAWebPageError422not_a_web_page: the address is a PDF, which has no page text (fetch, extract, and the pages search and crawl open). Never retried.
ModelRefusedError422model_refused: the model declined to extract from these pages (extract).
SearchUnavailableError503search_unavailable: web search is not set up, or its provider did not answer. Never repeated by the SDK.
OutOfViewportError400out_of_viewport: a mouse or computer-use action points outside the page.
WebhookUrlNotAllowedError400webhook_url_not_allowed: a webhook URL that is not public HTTPS.
WebhookDisabledError, PayloadExpiredError409webhook_disabled, payload_expired: a delivery that cannot be sent again.
WebhooksUnavailableError503webhooks_unavailable: webhooks are not set up on this API server.
WebhookSignatureErrornoneFrom verifyWebhook: reason is malformed, timestamp_out_of_range or no_matching_signature.
NotResumableError409not_resumable: the run is neither paused nor stopped early (it is running, completed, canceled, failed for another reason, or its resume window passed).
RunNotLiveError409run_not_live: the run's API server stopped; resume it once it shows as failed with server_restarted.
TooManyMessagesError409too_many_messages: the run already got 50 messages.
SessionNotRunningError409session_not_running: the session is stopped or deleted. A call on a stopped session does not resume it: call resume first. Reading its files works while it is stopped.
NothingSavedError409nothing_saved: resume, or reading the files of a stopped session, when it saved nothing (its machine was lost before its first checkpoint). Start a new session.
BrowserDisabledError409browser_disabled: a browser call (actions, computer, runScript, exportCookies, login) on a session created with browser: false.
ShellDisabledError409shell_disabled: a shell call (exec, restartShell, runScript) on a session created without shell: true.
NotUpdatableError400not_updatable: sessions.update named a field fixed at create (browser or shell on or off, viewport, profile, recordSession, setup, extensions, env, credentials). The message names it.
ArchiveTooLargeError413archive_too_large: the folder is over 1 GiB or 100,000 entries. Nothing was sent: download smaller folders, or single files.
ArchiveBusyError429archive_busy: two archives of this session are being made already. A kind of RateLimitError: try again when one is done.
InvalidPathError400 or 403invalid_path: a file path outside the workspace, or through a link.
NotADirectoryError400not_a_directory: files.archive of a path that is a file.
VariablesWithExtensionsError400variables_with_extensions: an agent run with variables in a session with extensions, without allowWithExtensions.
MissingVariablesError400missing_variables: a task run (or its schedule) has no value for a variable without a default.
CredentialExistsError409credential_exists: the project has a credential with that name; change it with credentials.update.
CredentialNotAllowedError400credential_not_for_ai, credential_not_for_shell: the credential's scope does not allow this use.
CredentialCodeTimeoutError408credential_code_timeout: no 2FA code or sign-in link arrived within codeTimeoutSeconds (session.login, typeCredential; boxline-otp exits with 1).
CredentialLinkWrongSiteError400credential_link_wrong_site: a sign-in link is not on one of the credential's sites, so it was not used.
CredentialLoginFailedError422credential_login_failed: session.login could not sign in. runId is the run that tried (bx.agent.get(runId) has its steps).
CredentialLoginTimeoutError408credential_login_timeout: session.login ran out of time and its run was canceled. A kind of CredentialLoginFailedError.
CodeUrlNotAllowedError400code_url_not_allowed: a credential's codeUrl is not a public HTTPS address.
TooManyCredentialValuesError409too_many_credential_values: the session already hides 256 earlier values. Start a new session.
MachineTooOldError409machine_too_old: during a platform update, the session's machine cannot take env or credentials.
PlanLimitError402plan_limit: the plan allows no more of this (tasks, schedules switched on, credentials, browser profiles, searches on Free, a longer session).
InvalidExtensionError400invalid_extension: the zip is not an acceptable Manifest V3 extension; the message says why.
ExtensionDeniedError403extension_denied: the platform does not allow this extension.
CrossSiteRequestError403cross_site_request: a console-login upload that did not come from the platform's own sites.
LimitReachedError409limit_reached: the project has as many as it may (100 extensions).
PayloadTooLargeError413payload_too_large: the body is too large (an extension zip over 10 MB).
AuthenticationError401A missing or wrong API key.
NotFoundError404No such thing in this project.
BoxlineConnectionErrornoneNo answer came back (the network failed). Retried by itself where safe.
BoxlineTimeoutErrornoneThe request took longer than its time limit.
TypeScript
import { Boxline, BoxlineError, FeatureNotInPlanError, RateLimitError } from "@boxline/sdk";const bx = new Boxline();try {  const session = await bx.sessions.create({ shell: true });  try {    console.log((await session.exec("python3 --version")).stdout);  } finally {    await session.stop(); // a failure never leaves a session running  }} catch (err) {  if (err instanceof RateLimitError) console.log("busy, try again in", err.retryAfter, "s");  else if (err instanceof FeatureNotInPlanError) console.log("not on this plan:", err.message);  else if (err instanceof BoxlineError) console.log(err.code, err.message, "request", err.requestId);  throw err;}

A failed single action, such as session.goto(), throws its own code when it has one (captcha_timeout), otherwise action_failed. See Errors and request ids.

Retries

GET requests, and the calls that create or start something (sessions, bulk, agent runs and resuming them, crawls, API keys and browser profiles), are tried again after a network error, a time-out, a 429 or a 5xx. That is 2 retries by default, waiting 0.5 to 8 seconds with some randomness, or as long as Retry-After or RateLimit-Reset say, up to 60 seconds. A longer wait comes back to you as a RateLimitError.

Other POST requests, such as exec, actions and fetch, are never retried, because they could run twice. See Rate limits.

Idempotency keys

The SDK sends a new Idempotency-Key with every create, and the same key on its retries. So a retry never starts a second session or run. To make your own retries safe too, for example after your process restarts, pass your own key, such as a job id:

TypeScript
const session = await bx.sessions.create({ timeout: 600 }, { idempotencyKey: "nightly-report-2026-09-30" });

The API remembers a key for 24 hours. The same key with a different body throws IdempotencyMismatchError. See Idempotency keys.

Options for one call

Every method takes the options below as its last argument.

OptionWhat it does
timeoutMsThe time limit of this call. Long calls (exec, files.waitFor, plain-English steps) get the time they need by themselves.
maxRetriesRetries for this call.
idempotencyKeyYour own key for a create.
clientRequestIdYour own id for the request (1 to 64 of A-Z a-z 0-9 . _ : -), sent as X-Client-Request-Id. The API logs it next to its own id, and errors carry it.
signalAn AbortSignal that stops the call and its retries.
headersExtra headers for this call.
TypeScript
await bx.sessions.get("SESSION_ID", { timeoutMs: 5_000, maxRetries: 0, clientRequestId: "trace-42" });const patient = bx.withOptions({ maxRetries: 6 }); // a copy of the client with other defaults

Every method

Each method calls one endpoint of the API reference, where every page has a Node example. The Python SDK has the same names in snake_case.

Sessions

MethodWhat it doesEndpoint
bx.sessions.create(params)Start a session: a browser (the default), a shell ({ browser: false, shell: true }), or both ({ shell: true }). Returns a Session.POST /v1/sessions
bx.sessions.get(id)Get a session. Returns a Session with fresh signed connectUrl, liveUrl and terminalUrl each time (null where the session has no browser or shell).GET /v1/sessions/{id}
session.refresh()Read the session again: fresh data, and fresh signed URLs.GET /v1/sessions/{id}
bx.sessions.list({ status, kind, q, from, to, sort, limit, after })Sessions, newest first; kind is browser, shell or combined. Await it for one page, or iterate it for all.GET /v1/sessions
bx.sessions.update(id, { keepAlive, timeout, idleTimeout, userMetadata, proxy, captcha, browser, blockAds, cookieBanners, rotateProxy, rotateUrls })The one call that changes a running session. timeout is its length in seconds from its last start. rotateProxy: true gives the proxy a new IP. rotateUrls: true revokes every earlier signed URL and returns new ones. proxy: null removes the proxy. A field fixed at create throws NotUpdatableError.PATCH /v1/sessions/{id}
bx.sessions.stop(id)Save it exactly as it is (the whole browser and its files), free the machine and stop billing. It is kept for your plan's retention days.POST /v1/sessions/{id}/stop
bx.sessions.resume(id)Bring a stopped session back on a fresh machine, as it was. Throws NothingSavedError when nothing was saved.POST /v1/sessions/{id}/resume
bx.sessions.delete(id)Delete it for good, with what it saved, its recording and its logs. It cannot be undone.DELETE /v1/sessions/{id}
bx.sessions.bulk(action, ids)Stop, resume or delete up to 100 sessions: bulk("stop" | "resume" | "delete", ids).POST /v1/sessions/bulk

Browser

MethodWhat it doesEndpoint
bx.sessions.actions(id, actions, { timeoutMs })Run a list of actions (and plain-English strings) in one call; each result has text.POST /v1/sessions/{id}/browser/actions
bx.sessions.computer(id, action, { maxWidth, screenshot })One computer-use action in Anthropic's or OpenAI's shape, and the screen after it.POST /v1/sessions/{id}/browser/computer
session.mouse.move(x, y) / moveBy(dx, dy) / click(x, y) / down() / up() / drag(from, to)The mouse, in CSS pixels of the page.POST /v1/sessions/{id}/browser/actions
session.hover(target) / cursor()Hover over an element or a point; where the pointer is.POST /v1/sessions/{id}/browser/actions
session.keyboard.key(keys) / type(text) / press(key)Key combinations such as Control+A, typing, one key.POST /v1/sessions/{id}/browser/actions
session.goto(url) / click(target) / fill(selector, value)One action each. They throw when the action fails.POST /v1/sessions/{id}/browser/actions
session.type(text) / press(key) / scroll(deltaY) / wait(target) / select(selector, option)Keyboard, scrolling, waiting and select boxes.POST /v1/sessions/{id}/browser/actions
session.elements() / evaluate(expression) / content(format)The page as a model sees it, a JavaScript value, or the page as text.POST /v1/sessions/{id}/browser/actions
session.screenshot({ fullPage, format, quality, maxWidth, cursor })A screenshot of the current tab: { data (base64), mimeType }, and width, height and scale with maxWidth or cursor.POST /v1/sessions/{id}/browser/actions
session.upload(selector, path)Put a file from the workspace into a file input.POST /v1/sessions/{id}/browser/actions
session.tabs() / newTab(url) / switchTab(index) / closeTab(index)Tabs.POST /v1/sessions/{id}/browser/actions
session.back() / forward() / reload()History.POST /v1/sessions/{id}/browser/actions
session.step(instruction, { variables, credentials, provider, model })One plain-English step; credentials lets it type saved credentials as %NAME%, %NAME.password% and so on.POST /v1/sessions/{id}/browser/actions
session.typeCredential(name, { field, selector })Type a credential into a field, without its value passing through your code (field: username, password or otp). With a codeSource of push or url, otp waits for the code your system sends.POST /v1/sessions/{id}/browser/actions
session.login(credential?, { url?, allowWithExtensions? })Sign the browser in with a password credential in one call (default: the one the session's profile links). Resolves to { url, title, runId }. Give the call a longer timeoutMs when the credential waits for a code.POST /v1/sessions/{id}/browser/actions
session.extract(instruction, { schema, provider, model })Structured data from the current page.POST /v1/sessions/{id}/browser/actions
bx.sessions.exportCookies(id, path?) / session.exportCookies(path?)The browser's cookies as a file in the workspace, for curl and wget. Resolves to { path, count }.POST /v1/sessions/{id}/browser/export-cookies

Shell, scripts and files

MethodWhat it doesEndpoint
bx.sessions.exec(id, command, { timeoutMs, cwd, env, credentials, shell }) / session.exec(command, opts)Run a shell command; cwd, env and credentials for this command only. A session without a shell throws ShellDisabledError. Start a long job in the background (nohup … > job.log 2>&1 &) and read job.log later.POST /v1/sessions/{id}/shell/exec
bx.sessions.execStream(id, command, onData, opts) / session.execStream(command, onData, opts)Run a command and stream its output as it happens.POST /v1/sessions/{id}/shell/exec
bx.sessions.runScript(id, code, { env, timeoutMs, ai, credentials, signal, onData }) / session.runScript(code, opts)Run a Playwright script inside the session. It needs a browser and a shell.POST /v1/sessions/{id}/browser/script
bx.sessions.restartShell(id, name?) / session.restartShell(name?)Restart a persistent shell.POST /v1/sessions/{id}/shell/restart
bx.sessions.files.read(id, path) / readText(id, path)A file's bytes, or its text. Works on a stopped session too.GET /v1/sessions/{id}/files
bx.sessions.files.list(id, path)A folder's entries. Works on a stopped session too.GET /v1/sessions/{id}/files
bx.sessions.files.archive(id, path?) / session.files.archive(path?)A folder and everything in it as one .tar.gz, as a Uint8Array (the whole workspace without a path). Works on a stopped session too.GET /v1/sessions/{id}/files/archive
bx.sessions.files.write(id, path, data)Write a file. Folders are made as needed. The session must be running.PUT /v1/sessions/{id}/files
bx.sessions.files.delete(id, path)Delete a file. The session must be running.DELETE /v1/sessions/{id}/files
bx.sessions.files.waitFor(id, pattern, timeoutMs)Wait for a finished file, such as a download. The session must be running.GET /v1/sessions/{id}/files/wait

Logs and CAPTCHAs

MethodWhat it doesEndpoint
bx.sessions.events(id, { types, after, limit })The session's events, oldest first. Every shell command is an exec event, whoever ran it: data.by is api, agent (with data.runId), setup or script. nextAfter is where to poll from.GET /v1/sessions/{id}/events
bx.sessions.streamEvents(id, { after })Events as they happen, with for await.GET /v1/sessions/{id}/events/stream
bx.sessions.pages(id)Pages visited.GET /v1/sessions/{id}/pages
bx.sessions.recording(id)The recording's frames.GET /v1/sessions/{id}/recording
bx.sessions.recordingFrame(id, index)One frame as JPEG bytes.GET /v1/sessions/{id}/recording/frames/{index}
session.waitForHuman({ timeoutMs })Wait until no CAPTCHA waits for a person. Throws CaptchaTimeoutError after 5 minutes by default.helper
session.onCaptcha(handler) → stop()Be told when a CAPTCHA appears and goes. Stops by itself when the session stops.helper

Web, agent, account and usage

MethodWhat it doesEndpoint
bx.profiles.create({ name, fromSession?, attach? }) / get(id) / list() / delete(id)Browser profiles: cookies and local storage that sessions start with.GET /v1/profiles
bx.profiles.update(id, { name?, credential? })Rename a profile, and/or link the password credential it signs in with (null unlinks it).PATCH /v1/profiles/{id}
bx.credentials.create({ name, type, … }) / list() / get(name) / update(name, patch) / delete(name)Credentials, write-only: a password (origins, username, password, codeSource, totpSecret, codeUrl, codeTimeoutSeconds) or a secret (value). No call returns a value. With a codeUrl, create and update also return codeUrlSecret, once. update(name, { rotateCodeUrlSecret: true }) makes a new codeUrlSecret, shown once; the old one stops at once.POST /v1/credentials
bx.credentials.pushCode(name, { code } | { link })Send the 2FA code or sign-in link a site emailed or texted to a run, action or boxline-otp that waits for it (codeSource push). Used once, kept 10 minutes. Not retried.POST /v1/credentials/{name}/codes
bx.credentials.audit({ name })Changes to credentials, and each use, newest first.GET /v1/credentials/audit
bx.extensions.upload(zip) / list() / get(id) / delete(id)Chrome extensions: a path or the zip's bytes; an Idempotency-Key is sent.POST /v1/extensions
bx.fetch(url, { format, links, timeoutMs, waitUntil, proxy, browser })A page as Markdown, HTML or text.POST /v1/fetch
bx.screenshot(url, opts)A screenshot, as a Uint8Array.POST /v1/screenshot
bx.pdf(url, opts)A PDF, as a Uint8Array.POST /v1/pdf
bx.extract({ url | urls, prompt, schema, … })JSON from pages.POST /v1/extract
bx.search({ query, limit, country, language, recency, safeSearch, fetch, proxy })Web search results, and the top pages as Markdown with fetch.POST /v1/search
bx.crawl.start(params) / get(id, { limit, after }) / list() / cancel(id)Crawls. maxPages goes up to your plan's ceiling: 200 on Free, 1000 on paid plans. robots.txt is always obeyed.POST /v1/crawls
bx.crawl.pages(id) / wait(id)Every page of a crawl, or wait for it to finish, then every page.helper
bx.agent.run(params) / get(id) / list() / cancel(id)Agent runs. session is the run's own session, with the fields sessions.create takes (a shell-only run: session: { browser: false, shell: true }); mode: "computer" for computer mode; output (a JSON Schema) for a JSON answer, typed with get<T> and wait<T>. A run that stopped early has resumable: { until }.POST /v1/agent/runs
bx.agent.pause(id, reason?)Pause a run: the AI finishes its step and waits (status paused), so a person can act in the live view or the terminal. Works on every kind of run.POST /v1/agent/runs/{id}/pause
bx.agent.resume(id, { note?, maxSteps?, maxCostUsd?, variables? })Keep a run going: the same run, with the same id; its steps and usage add up. note is one text for the AI (up to 2,000 characters): what a person did after a pause, or what to do next after a limit. A run that stopped early (a limit, or a server restart) also takes new limits and its variables again.POST /v1/agent/runs/{id}/resume
bx.agent.sendMessage(id, text)Tell a working run something; it reads it at its next step.POST /v1/agent/runs/{id}/messages
bx.agent.stream(id)A run's events as they happen (thought, steps, status…); ends after the done event.GET /v1/agent/runs/{id}/events/stream
bx.agent.wait(id)Poll until the run finishes (a paused run keeps waiting).helper
bx.agent.models()Providers and models.GET /v1/agent/models
bx.tasks.create(params) / list() / get(id) / update(id, patch) / delete(id)Tasks: saved agent runs with variables, an output schema and a schedule (null removes a field on update).POST /v1/tasks
bx.tasks.run<T>(id, { variables, sessionId }) / runs(id, { status })Run a task now; its run history, newest first.POST /v1/tasks/{id}/runs
bx.tasks.waitForRun(run)Poll until a task run finishes, and return it with its result.helper
bx.me() / hasFeature(feature)Your project, its plan's limits and features; one feature.GET /v1/auth/me
bx.project.settings() / setSettings({ captchaDefault, defaultModel, trajectories })The project settings: the CAPTCHA default, the default model (null clears it) and trajectories: { enabled, noticeSeenAt, decidedBy }. setSettings({ trajectories: { enabled } }) turns the Trajectories program on or off.GET /v1/project/settings
bx.project.modelKeys() / setModelKey(provider, { key, use }) / deleteModelKey(provider)Your own model keys: list (never the key), save or switch, delete.GET /v1/project/model-keys
bx.webhooks.create({ url, events }) / list() / get(id) / update(id, patch) / delete(id)Webhook endpoints (the secret only on create). update(id, { rotateSecret: true }) makes a new signing secret, shown once; the old one keeps signing for 24 hours.POST /v1/webhooks
bx.webhooks.eventTypes()Every event type an endpoint can subscribe to.GET /v1/webhooks/event-types
bx.webhooks.test(id, { type? })Send a test event now, or a sample of one type.POST /v1/webhooks/{id}/test
bx.webhooks.deliveries(id, { status }) / retryDelivery(id, deliveryId)The delivery log, and sending one again.GET /v1/webhooks/{id}/deliveries
verifyWebhook(body, header, secret | [secrets])Checks a delivery's signature and returns the event (no request).helper
bx.apiKeys.list() / create({ name }) / revoke(id)API keys.GET /v1/project/api-keys
bx.usage({ from, to })What the project used and spent in a period: sessions and how many run now (running, concurrencyLimit), agent runs and their model cost (agentRuns, modelCostUsd, ownKeyModelCostUsd), seconds, proxy data, CAPTCHA solving and searches, and the same per day in byDay.GET /v1/project/usage
bx.pricing() / openapi() / health()Prices and plans, the OpenAPI description, and a health check.GET /v1/pricing
bx.request(method, path, body?, options?)Any endpoint, returning the parsed JSON.helper
bx.send(method, path, body?, options?)Any endpoint, returning the raw Response (streams).helper
bx.bytes(method, path, body?, options?)Any endpoint, returning the body as a Uint8Array (files, images).helper

Prefer another language? Everything the SDKs do works over the plain HTTP API. The Python SDK has the same shape.

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