Docs
Dashboard
SDKsBeta

Python SDK

The Python SDK: install it, create a client, and every method it has.

The Python SDK (boxline-sdk on PyPI, imported as boxline) has a method for every endpoint of the HTTP API, named in snake_case. It retries safely, sends idempotency keys for you, pages through lists, and raises typed errors. There is a normal client, Boxline, and an async one, AsyncBoxline, with the same methods.

It needs Python 3.9 or newer. Results are plain dicts with the API’s own field names (camelCase), typed in boxline.types. Sessions are Session objects.

Install

Shell
pip install boxline-sdk playwright

playwright is only needed to drive the browser with Playwright.

Install boxline-sdk, not boxline: the name boxline on PyPI belongs to an unrelated project. The package you install is boxline-sdk, and you import boxline.

Open source (MIT licence): github.com/Boxline-dev/sdk-python. The package on PyPI: 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"
Python
import osfrom boxline import Boxlinebx = Boxline()  # reads BOXLINE_API_KEY and BOXLINE_API_URL# Or set the options yourself. timeout is in seconds, for each request.custom = Boxline(    api_key=os.environ["BOXLINE_API_KEY"],    base_url="https://api.boxline.dev",    max_retries=3,    timeout=60,)# As a context manager, the client closes its connections at the end.with Boxline() as client:    print(client.me()["project"]["plan"])
OptionDefaultWhat it does
api_keyBOXLINE_API_KEYYour API key.
base_urlBOXLINE_API_URL, then the public APIThe API’s address, https://api.boxline.dev.
max_retries2How often a safe request is tried again. See Retries.
timeout120The time limit of one request, in seconds.
headersnoneHeaders sent with every request.
http_clienta new httpx.ClientYour own httpx client, for proxies or tracing.

Sessions and Playwright

A session is an isolated machine with a browser (the default), a shell, or both, and a /workspace disk they share. Leaving the with block stops it, also after an error. Connect Playwright to a browser session’s connect_url, or drive the same browser with the actions API.

Python
from playwright.sync_api import sync_playwrightfrom boxline import Boxlinebx = Boxline()with bx.sessions.create(timeout=300) as session:  # leaving the block stops it    with sync_playwright() as p:        # A signed URL: treat it like a password.        browser = p.chromium.connect_over_cdp(session.connect_url)        page = browser.contexts[0].pages[0]        page.goto("https://example.com")        print(page.title())    # The actions API drives the same browser without Playwright.    print(session.content("markdown")["title"])

session.live_url 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 raises SessionNotRunningError.

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

Every method that works on one session is on bx.sessions and takes the session id first, such as bx.sessions.stop(session_id). A Session has the same methods without the id, such as session.stop(). Its fields read in snake_case (session.expires_at), and session.data holds the whole record.

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.

Python
browser_only = bx.sessions.create()                              # the defaultshell_only = bx.sessions.create(browser=False, shell=True)both = bx.sessions.create(shell=True)

A browser call (actions, computer, run_script, export_cookies, and the session.goto() family) on a shell-only session raises BrowserDisabledError. A shell call (exec, restart_shell) on a session without a shell raises ShellDisabledError.

Change a running session

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

Python
with bx.sessions.create(timeout=300, proxy={"type": "residential", "country": "DE"}) as session:    session.update(timeout=900)           # its length in seconds from its last start; expires_at moves    session.update(idle_timeout=120)      # end it after 2 minutes without activity    session.update(rotate_proxy=True)     # a new proxy IP now; open browser connections reconnect (400 without a proxy)    session.update(proxy=None)            # remove the proxy    session.update(block_ads=True, cookie_banners="reject", captcha="ask")    session.update(rotate_urls=True)      # every earlier signed URL stops working    print(session.live_url)               # 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 raises PlanLimitError). A longer session bills for longer: set idle_timeout so a session whose client crashed ends by itself.
  • rotate_urls=True is for a URL that leaked, or for a person who should lose access. Every earlier connect_url, live_url and terminal_url 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, record_session, setup, extensions, env, credentials) are not arguments of update: set them in sessions.create. Sent to the API by hand (with bx.request), they answer 400 not_updatable naming the field, which the SDK raises as NotUpdatableError.
  • A session that is not running raises 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.py
import urllib.parsefrom boxline import Boxlinefrom playwright.sync_api import sync_playwrightbx = Boxline()with bx.sessions.create(shell=True, keep_alive=True) as session:    # Browser: download a report. Downloads land in /workspace/downloads.    # (A demo page; on your own site, page.goto() your report page instead.)    with sync_playwright() as p:        browser = p.chromium.connect_over_cdp(session.connect_url)        context = browser.contexts[0]        page = context.pages[0] if context.pages else context.new_page()        csv_url = "data:text/csv," + urllib.parse.quote("month,revenue\nJan,1200\nFeb,1850\nMar,2410")        page.set_content(f'<a download="report.csv" href="{csv_url}">Download report</a>')        page.get_by_role("link", name="Download report").click()        csv = session.files.wait_for("downloads/*.csv")        print("downloaded", csv["path"], csv["size"])    # Shell: add up the revenue column with Python, on the same machine.    run = 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:        raise RuntimeError(run["stderr"])    # Files: read the result.    total = session.files.read_text("output/total.txt").strip()  # "5460"    # Actions: type it into the form and send it.    results = 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"},    ])    print(results[-1]["value"]["content"])  # what the form received

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.

Python
tar = session.files.archive("output")  # a folder as one .tar.gz (bytes)with open("output.tar.gz", "wb") as f:    f.write(tar)session.stop()print(session.files.read_text("output/total.txt"))  # still readable while stoppedentries = session.files.list("output")

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

The page APIs

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

Python
page = bx.fetch("https://example.com", format="markdown", links=True)png = bx.screenshot("https://example.com", full_page=True)  # bytespdf = bx.pdf("https://example.com", paper="A4")result = bx.extract(url="https://example.com/pricing", prompt="The name and monthly price of every plan")print(result["data"])started = bx.crawl.start("https://example.com/docs", max_pages=20)crawl = bx.crawl.wait(started["id"])  # every page in crawl["data"]

A page that cannot load raises 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 raising. See Web.

Python
found = bx.search("playwright connectOverCDP", limit=5, recency="year")for r in found["results"]:    print(r["title"], r["url"], r.get("publishedAt", ""))# fetch: also open the top pages (True = 3, or 0 to 5) and get them as Markdown.with_pages = bx.search("playwright connectOverCDP", limit=3, fetch=2)print((with_pages["results"][0].get("content") or "")[:200], with_pages["results"][0].get("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.

Python
with bx.sessions.create() as s:    s.goto("https://example.com")    s.mouse.move(400, 300, steps=10)    s.mouse.click(400, 300, button="right", count=2, modifiers=["Shift"])    s.mouse.drag("#card", "#done-column", steps=20)         # selectors: the element's centre    s.mouse.drag(path=[(100, 100), (200, 150), (300, 100)])  # or a path    s.mouse.down()    s.mouse.up()    s.hover("#menu")    s.keyboard.key("ControlOrMeta+A")    print(s.cursor())                                        # {"x", "y"}    png = s.screenshot(max_width=640, cursor=True)    # Every result says what happened; a bare string is a plain-English step.    results = s.actions([{"action": "goto", "url": "https://example.com"}, "click More information"])    print([r.get("text") for r in results])    # One action exactly as a computer-use model gave it, and the screen after it.    screen = s.computer({"action": "left_click", "coordinate": [512, 300]}, max_width=1024)

A point outside the page raises OutOfViewportError. See Mouse and keyboard and Computer use. 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.

Python
import osrun = bx.agent.run(    "Sign in to https://example.com with %email% and %password%, then open the billing page.",    variables={        "email": "ada@example.com",        "password": {"value": os.environ["SITE_PASSWORD"], "origins": ["https://example.com"]},    },)for event in bx.agent.stream(run["id"]):    if event["type"] == "thought":        print("…", event["text"])  # what it is about to do    elif event["type"] == "tool":        print(event["name"], event.get("input"))  # placeholders, never values    elif event["type"] == "done":        print(event["status"], event.get("result"))

bx.agent.wait(run_id) polls until the run ends. A run works in its own session. Describe it with session, a dict that takes the fields sessions.create takes, written with the API’s names. A run in a session without a browser has no browser tools. See AI agent.

Python
# A shell-only run: no browser, so the AI gets the shell and the files only.run = bx.agent.run(    "Convert every video in /workspace to MP4 with ffmpeg and list the new files",    session={"browser": False, "shell": True},)done = bx.agent.wait(run["id"])print(done["status"], done["result"])

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

Pause and resume a run

bx.agent.pause(run_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(run_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 says until when that works.

Python
run = bx.agent.run("Download the latest invoice from https://example.com/billing")bx.agent.pause(run["id"], "I will sign in myself")      # the AI finishes its step, then waitssession = bx.sessions.get(run["sessionId"])print(session.live_url)                                 # a signed link: share it only with that personbx.agent.resume(run["id"], note="I signed in")          # the same run goes ondone = bx.agent.wait(run["id"])if done.get("resumable"):    # It stopped at a limit. Give it more steps; the work so far is kept.    bx.agent.resume(run["id"], max_steps=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 raises NotResumableError. See Pause and resume.

A script with useModel

run_script runs Playwright code (JavaScript) 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.

Python
code = """useModel("claude-haiku-5-5");                               // every later step() and extract() uses itawait 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 onlyconsole.log(JSON.stringify(await extract("the plan names and prices")));"""with bx.sessions.create(shell=True) as s, s.run_script(code, env={"email": "ada@example.com"}) as proc:    for stream, text in proc:        print(text, end="")print(proc.result["exitCode"])

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.

Python
def tell_someone(change):    # The live URL is a signed link: share it only with the person who may solve it.    if change["state"] == "detected":        print(f'{change["kind"]} on {change["url"]}: solve it in the live view')with bx.sessions.create(captcha="ask") as s:    stop = s.on_captcha(tell_someone)  # runs on a background thread until the session stops, or stop()    s.goto("https://example.com/signup")    s.wait_for_human(timeout=300)      # raises CaptchaTimeoutError if nobody solves it    stop()

See CAPTCHAs.

Async

AsyncBoxline has the same methods. Await them, and use async for and async with.

Python
import asynciofrom boxline import AsyncBoxlineasync def main():    async with AsyncBoxline() as bx:        pages = await asyncio.gather(*(bx.fetch(u) for u in ["https://example.com", "https://example.org"]))        async with await bx.sessions.create() as s:            print((await s.goto("https://example.com"))["title"])        async for session in bx.sessions.list(status="RUNNING"):            print(session.id)asyncio.run(main())

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:

Python
s = bx.sessions.create(block_ads=True, cookie_banners="reject")  # "reject" is the defaults.goto("https://example.com")s.refresh()print(s.blocked_requests)  # refused inside the machine, before any proxys.update(block_ads=False, cookie_banners="off")bx.fetch("https://example.com", block_ads=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.

Python
ext = bx.extensions.upload("my-extension.zip")  # a path, or the zip's bytes; at most 10 MBwith_ext = bx.sessions.create(extensions=[ext["id"]])  # loaded before the session is returnedfor e in bx.extensions.list():    print(e["id"], e["name"], e["version"], e["permissions"])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 raises VariablesWithExtensionsError unless you pass allow_with_extensions=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 code_source="push" and send each code with credentials.push_code while the run waits for it. With code_source="url" and a code_url, the platform asks your endpoint instead, and verify_webhook checks each request.

Python
import osfrom boxline import CredentialCodeTimeoutError, CredentialLoginFailedErrorbx.credentials.create(    "SHOP",    "password",    origins=["https://shop.example.com"],    username="ops@example.com",    password=os.environ["SHOP_PASSWORD"],    code_source="push",  # your system sends each code or sign-in link)session = bx.sessions.create()try:    # login waits as long as the server may (a code from your system included).    page = session.login("SHOP", url="https://shop.example.com/signin")    print(page["url"], page["title"], page["runId"])except CredentialLoginFailedError as err:  # also CredentialLoginTimeoutError    print("could not sign in; run", err.run_id)except CredentialCodeTimeoutError:    print("no code arrived in time")# In your webhook receiver, when credential.code_needed arrives (the run is waiting):bx.credentials.push_code("SHOP", code="123456")  # what the site just emailed or texted

credentials.update(name, rotate_code_url_secret=True) makes a new signing secret for a code_url and returns it once. The old secret stops at once, so put the new one into your endpoint straight away. See Credentials.

Webhooks

Python
import osfrom boxline import WebhookSignatureError, verify_webhookendpoint = bx.webhooks.create("https://example.com/webhooks/boxline", ["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:    event = verify_webhook(body, header, os.environ["BOXLINE_WEBHOOK_SECRET"])  # 5-minute window    # drop event["id"] if you have handled it already, then answer 200except WebhookSignatureError as e:    print(e.reason)  # answer 400for d in bx.webhooks.deliveries(endpoint["id"], status="failed"):    print(d["id"], d["errorCode"])bx.webhooks.test(endpoint["id"])

webhooks.update(id, rotate_secret=True) makes a new signing secret and returns it once. The old one keeps signing for 24 hours, so give verify_webhook both while you switch. retry_delivery(id, delivery_id), update and delete do the rest, and webhook_signature_header(secret, body) signs bodies for your receiver’s tests. See Rotate a signing secret and Webhooks.

Project settings

Python
settings = bx.project.settings()  # {"captchaDefault", "captchaDefaultEffective", "trajectories", "updatedVia", …}print(settings["trajectories"])   # {"enabled", "noticeSeenAt", "decidedBy"}bx.project.set_settings(captcha_default="ignore")                # for new sessions and runs without captchabx.project.set_settings(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.

Python
u = bx.usage(from_="2026-10-01", to="2026-10-07")print(f'{u["running"]} of {u["concurrencyLimit"]} sessions running')print(u["sessions"], u["agentRuns"], u["modelCostUsd"], u["ownKeyModelCostUsd"], u["costUsd"])for day in u["byDay"]:    print(day["date"], day["sessions"], day["agentRuns"], day["costUsd"])

See Pricing.

Lists and pages

Every list takes limit and after. A list call returns its first page. Iterating it yields every item: the SDK follows next for you.

Python
page = bx.sessions.list(status=["RUNNING", "STOPPED"], limit=50)print(len(page.data), page.total, page.next)for s in bx.sessions.list(status="RUNNING", kind="shell"):  # shell-only sessions    print(s.id)for e in bx.sessions.events("SESSION_ID", types=["console", "error"]):    print(e.get("text"))for p in page.iter_pages():    print(len(p.data))

With AsyncBoxline, await bx.sessions.list() is the first page and async for s in bx.sessions.list() goes through all of them. The lists are sessions.list, sessions.events, sessions.pages, profiles.list, api_keys.list, agent.list and crawl.list. A crawl’s pages come from crawl.get(crawl_id, after=…), or all of them from for p in bx.crawl.pages(crawl_id). See Pagination.

Errors

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

ClassStatusWhen
RateLimitError429rate_limited or concurrency_limit. retry_after 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 wait_for_human).
PageUnreachableError502page_unreachable: the page could not be loaded (fetch, screenshot, pdf, extract).
PageTimeoutError504page_timeout: the page did not finish loading within timeout_ms.
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 verify_webhook: 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, run_script, export_cookies, login) on a session created with browser=False.
ShellDisabledError409shell_disabled: a shell call (exec, restart_shell, run_script) 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, record_session, 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. Import it as boxline.NotADirectoryError: Python has a built-in of the same name.
VariablesWithExtensionsError400variables_with_extensions: an agent run with variables in a session with extensions, without allow_with_extensions.
MissingVariablesError400missing_variables: a task run (or its schedule) has no value for a variable without a default.
PlanLimitError402plan_limit: the plan allows no more of this (tasks, schedules switched on, credentials, browser profiles, searches on Free, a longer session).
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, type_credential; 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. run_id is the run that tried (bx.agent.get(run_id) 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 code_url 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.
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.
Python
from boxline import Boxline, BoxlineError, FeatureNotInPlanError, RateLimitErrorbx = Boxline()try:    with bx.sessions.create(shell=True) as session:  # stopped even when the code inside fails        print(session.exec("python3 --version")["stdout"])except RateLimitError as err:    print("busy, try again in", err.retry_after, "s")except FeatureNotInPlanError as err:    print("not on this plan:", err.message)except BoxlineError as err:    print(err.code, err.message, "request", err.request_id)    raise

A failed single action, such as session.goto(), raises 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:

Python
session = bx.sessions.create(timeout=600, options={"idempotency_key": "nightly-report-2026-09-30"})

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

Options for one call

Every method takes options=, a dict with these keys:

KeyWhat it does
timeoutThe time limit of this call, in seconds. Long calls (exec, files.wait_for, plain-English steps) get the time they need by themselves.
max_retriesRetries for this call.
idempotency_keyYour own key for a create.
client_request_idYour 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.
headersExtra headers for this call.
Python
bx.sessions.get("SESSION_ID", options={"timeout": 5, "max_retries": 0, "client_request_id": "trace-42"})patient = bx.with_options(max_retries=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 Python example. The names are the Node SDK’s, in snake_case.

Sessions

MethodWhat it doesEndpoint
bx.sessions.create(browser=True, shell=False, timeout=None, …)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(session_id)Get a session. Returns a Session with fresh signed connectUrl, liveUrl and terminalUrl each time (None 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. The first page; iterate it for all.GET /v1/sessions
bx.sessions.update(session_id, keep_alive=, timeout=, idle_timeout=, user_metadata=, proxy=, captcha=, browser=, block_ads=, cookie_banners=, rotate_proxy=, rotate_urls=)The one call that changes a running session. timeout is its length in seconds from its last start. rotate_proxy=True gives the proxy a new IP. rotate_urls=True revokes every earlier signed URL and returns new ones. proxy=None removes the proxy. A field fixed at create raises NotUpdatableError.PATCH /v1/sessions/{id}
bx.sessions.stop(session_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(session_id)Bring a stopped session back on a fresh machine, as it was. Raises NothingSavedError when nothing was saved.POST /v1/sessions/{id}/resume
bx.sessions.delete(session_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(session_id, actions, timeout_ms=None)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(session_id, action, max_width=, 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) / move_by(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(selector= or x=, y=) / 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(selector) / fill(selector, value)One action each. They raise when the action fails.POST /v1/sessions/{id}/browser/actions
session.type(text) / press(key) / scroll(delta_y) / wait(selector=, ms=) / 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(full_page=False, format="png", max_width=, cursor=)A screenshot of the current tab, as bytes.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() / new_tab(url) / switch_tab(index) / close_tab(index)Tabs.POST /v1/sessions/{id}/browser/actions
session.back() / forward() / reload()History.POST /v1/sessions/{id}/browser/actions
session.step(instruction, variables=, provider=, model=, credentials=)One plain-English step; credentials= lets it type saved credentials as %NAME%, %NAME.password% and so on.POST /v1/sessions/{id}/browser/actions
session.type_credential(name, field=, selector=)Type a credential into a field, without its value passing through your code (field: username, password or otp). With a code_source of push or url, otp waits for the code your system sends.POST /v1/sessions/{id}/browser/actions
session.login(credential=, url=, allow_with_extensions=)Sign the browser in with a password credential in one call (default: the one the session's profile links). Returns {url, title, runId}. Give the call a longer options={"timeout": seconds} 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.export_cookies(session_id, path=None) / session.export_cookies(path=None)The browser's cookies as a file in the workspace, for curl and wget. Returns {path, count}.POST /v1/sessions/{id}/browser/export-cookies

Shell, scripts and files

MethodWhat it doesEndpoint
bx.sessions.exec(session_id, command, timeout_ms=, cwd=, env=, shell=, credentials=) / session.exec(command, …)Run a shell command; cwd, env and credentials for this command only. A session without a shell raises 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.exec_stream(session_id, command, …) / session.exec_stream(command, …)Run a command and stream its output: iterate (stream, text) pairs.POST /v1/sessions/{id}/shell/exec
bx.sessions.run_script(session_id, code, env=, timeout_ms=, ai=) / session.run_script(code, …)Run a Playwright script inside the session, streaming its output. It needs a browser and a shell.POST /v1/sessions/{id}/browser/script
bx.sessions.restart_shell(session_id, name=None) / session.restart_shell(name=None)Restart a persistent shell.POST /v1/sessions/{id}/shell/restart
bx.sessions.files.read(session_id, path) / read_text(session_id, path)A file's bytes, or its text. Works on a stopped session too.GET /v1/sessions/{id}/files
bx.sessions.files.list(session_id, path)A folder's entries. Works on a stopped session too.GET /v1/sessions/{id}/files
bx.sessions.files.archive(session_id, path=None) / session.files.archive(path=None)A folder and everything in it as one .tar.gz, as bytes (the whole workspace without a path). Works on a stopped session too.GET /v1/sessions/{id}/files/archive
bx.sessions.files.write(session_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(session_id, path)Delete a file. The session must be running.DELETE /v1/sessions/{id}/files
bx.sessions.files.wait_for(session_id, pattern, timeout_ms=30000)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(session_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. page.next_after is where to poll from.GET /v1/sessions/{id}/events
bx.sessions.stream_events(session_id, after=None)Events as they happen.GET /v1/sessions/{id}/events/stream
bx.sessions.pages(session_id)Pages visited.GET /v1/sessions/{id}/pages
bx.sessions.recording(session_id)The recording's frames.GET /v1/sessions/{id}/recording
bx.sessions.recording_frame(session_id, index)One frame as JPEG bytes.GET /v1/sessions/{id}/recording/frames/{index}
session.wait_for_human(timeout=300, interval=1)Wait until no CAPTCHA waits for a person. Raises CaptchaTimeoutError after that.helper
session.on_captcha(handler, interval=2.0) → stop()Be told when a CAPTCHA appears and goes: handler({"state", "kind", "url", "event"}), with state "detected" or "cleared". Stops by itself when the session stops.helper

Web, agent, account and usage

MethodWhat it doesEndpoint
bx.profiles.create(name, from_session=, 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 (None unlinks it).PATCH /v1/profiles/{id}
bx.credentials.create(name, type, origins=, username=, password=, code_source=, totp_secret=, code_url=, code_timeout_seconds=, value=, description=, shell=, scope=) / list() / get(name) / update(name, …) / delete(name)Credentials, write-only: a password or a secret. No call returns a value. With a code_url, create and update also return codeUrlSecret, once. update(name, rotate_code_url_secret=True) makes a new codeUrlSecret, shown once; the old one stops at once.POST /v1/credentials
bx.credentials.push_code(name, code=) / push_code(name, 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 (code_source 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="markdown", links=, timeout_ms=, wait_until=, proxy=, browser=)A page as Markdown, HTML or text.POST /v1/fetch
bx.screenshot(url, full_page=False, …)A screenshot, as bytes.POST /v1/screenshot
bx.pdf(url, paper="A4", …)A PDF, as bytes.POST /v1/pdf
bx.extract(url=, urls=, prompt=, schema=, …)JSON from pages.POST /v1/extract
bx.search(query, limit=, country=, language=, recency=, safe_search=, fetch=, proxy=)Web search results, and the top pages as Markdown with fetch.POST /v1/search
bx.crawl.start(url, max_pages=, …) / get(id, limit=, after=) / list() / cancel(id)Crawls. max_pages 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(task, session=, session_id=, variables=, mode=, output=, …) / get(id) / list() / cancel(id)Agent runs. session= is the run's own session, a dict 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. A run that stopped early has resumable: {"until": …}.POST /v1/agent/runs
bx.agent.pause(id, reason=None)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=None, max_steps=, max_cost_usd=, 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.send_message(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; 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(name, instruction, variables=, output=, schedule=, …) / list() / get(id) / update(id, …) / delete(id)Tasks: saved agent runs with variables, an output schema and a schedule (None removes a field on update).POST /v1/tasks
bx.tasks.run(id, variables=, session_id=) / runs(id, status=)Run a task now; its run history, newest first.POST /v1/tasks/{id}/runs
bx.tasks.wait_for_run(run)Poll until a task run finishes, and return it with its result.helper
bx.me() / has_feature(feature)Your project, its plan's limits and features; one feature.GET /v1/auth/me
bx.project.settings() / set_settings(captcha_default=, default_model=, trajectories=)The project settings: the CAPTCHA default, the default model (None clears it) and trajectories: {"enabled", "noticeSeenAt", "decidedBy"}. set_settings(trajectories={"enabled": False}) turns the Trajectories program on or off.GET /v1/project/settings
bx.project.model_keys() / set_model_key(provider, key=, use=) / delete_model_key(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, …) / delete(id)Webhook endpoints (the secret only on create). update(id, rotate_secret=True) makes a new signing secret, shown once; the old one keeps signing for 24 hours.POST /v1/webhooks
bx.webhooks.event_types()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=) / retry_delivery(id, delivery_id)The delivery log, and sending one again.GET /v1/webhooks/{id}/deliveries
verify_webhook(body, header, secret or [secrets])Checks a delivery's signature and returns the event (no request).helper
bx.api_keys.list() / create(name) / revoke(key_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=None, params=None, options=None)Any endpoint, returning the parsed JSON.helper

Differences from the Node SDK

PythonNode
sessions.create(browser_options={"mode": "realistic"}); browser is the on/off switchsessions.create({ browser: { mode: "realistic" } })
sessions.create(profile="id", persist_profile=True)sessions.create({ profile: { id, persist: true } })
session.update(browser={"mode": "realistic"}): here browser holds the settings, because update cannot switch the browser on or offsession.update({ browser: { mode: "realistic" } })
agent.run(session={…}) and tasks.create(session={…}) take a dict with the API’s own names (blockAds, userMetadata)The same names as sessions.create
session.on_captcha(handler): the sync client runs the handler on a background thread; the async client takes a coroutine functionsession.onCaptcha(handler)
session.screenshot() returns bytessession.screenshot() returns { data (base64), mimeType }
exec_stream and run_script return a stream to iterateexecStream and runScript take an onData function
Time limits in seconds (timeout)Time limits in milliseconds (timeoutMs)

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