Boxline Python SDK
Give your AI agents the infrastructure they need: browsers, shells, storage and isolated machines.
Each session is an isolated machine with a real Chrome and, if you
ask for it, a bash shell with Python and Node, sharing one /workspace disk. Sync (Boxline) and async
(AsyncBoxline) clients with the same methods.
pip install boxline-sdk # add [playwright] to drive the browser with Playwright: "boxline-sdk[playwright]"
export BOXLINE_API_KEY=bxl_...
The package is boxline-sdk on PyPI (the name boxline there is another project); you import it as boxline.
The client talks to https://api.boxline.dev; set BOXLINE_API_URL (or base_url=) for another API, e.g.
http://localhost:8080 for a local one.
Python 3.9 or newer. Responses are plain dicts with the API's camelCase keys, typed with TypedDicts
(boxline.types); the package ships py.typed.
- Sessions and Playwright
- Browser, shell and files together
- The page APIs
- Web search
- Mouse, keyboard and computer use
- An agent run with variables
- Limits, Continue and messages
- Tasks and structured output
- Credentials and browser profiles
- A script with useModel
- CAPTCHAs
- Browser settings and extensions
- Webhooks
- Async
- Lists and pages
- Errors, retries and time limits
- Every method
- Examples
Sessions and Playwright
from playwright.sync_api import sync_playwright
from boxline import Boxline
bx = Boxline() # BOXLINE_API_KEY; BOXLINE_API_URL (default https://api.boxline.dev)
with bx.sessions.create(timeout=300) as session: # leaving the block releases it
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.connect_url) # a signed URL: treat it like a password
page = browser.contexts[0].pages[0]
page.goto("https://example.com")
print(page.title())
# The actions API drives the same browser without Playwright, one HTTP call per list of actions.
print(session.content("markdown")["title"])
session.live_url is a page where a person can watch and take over. session.pause() saves the browser and files
and stops billing; touching the session resumes it.
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 the workspace, and the files API reads and writes it.
with bx.sessions.create(shell=True) as s, sync_playwright() as p:
page = p.chromium.connect_over_cdp(s.connect_url).contexts[0].pages[0]
page.goto("http://127.0.0.1:4800")
page.click("#download") # the browser downloads a CSV
s.files.wait_for("downloads/*.csv") # files: wait until it has finished writing
s.exec("""mkdir -p output && python3 - <<'EOF'
import csv
total = sum(float(row["revenue"]) for row in csv.DictReader(open("downloads/report.csv")))
open("output/total.txt", "w").write(f"{total:.2f}")
EOF""") # the shell: Python adds up a column
total = s.files.read_text("output/total.txt").strip() # files: read what the shell wrote
s.actions([{"action": "fill", "selector": "#total", "value": total}, {"action": "click", "selector": "#send"}])
The same mix works inside an agent run (shell=True: the agent uses browser_* tools and bash) and inside a script
(require("child_process") and require("fs") next to page and step()): see
agent_mixed.py and script_mixed.py.
The page APIs
One call each; the page renders in a real browser inside a sandbox.
page = bx.fetch("https://example.com", format="markdown", links=True)
png = bx.screenshot("https://example.com", full_page=True) # bytes
pdf = bx.pdf("https://example.com", paper="A4")
data = bx.extract(url="https://example.com/pricing", schema={"type": "object", "properties": {"plans": {"type": "array"}}})["data"]
job = bx.crawl.wait(bx.crawl.start("https://example.com/docs", max_pages=20)["id"])
A page that cannot load raises PageUnreachableError (502 page_unreachable) or PageTimeoutError (504
page_timeout).
Web search
found = bx.search("playwright connectOverCDP", limit=5, country="US", 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–5) in a sandboxed browser and get them as Markdown.
with_pages = bx.search("boxline docs", limit=3, fetch=2)
print((with_pages["results"][0].get("content") or "")[:200], with_pages["results"][0].get("error"))
The same search by the same project within an hour comes from the cache (cached: True) and is not counted; the plan
includes searchesPerMonth (bx.usage()["searches"]). The query text goes to the search provider (Brave) with the
platform's key, nothing about you: keep passwords and personal data out of it. SearchUnavailableError (503
search_unavailable) when search is not set up on the server.
Mouse, keyboard and computer use
Coordinates are CSS pixels of the session's viewport (1280×720 by default). Everything acts on the page through the
browser, never on the machine's desktop, and moves in straight lines (steps spreads a move over evenly spaced points).
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 mean 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") # Playwright's key names; ctrl, cmd, Return work too
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])
Computer use. session.computer() runs ONE action exactly as a computer-use model's tool gave it (Claude's
computer tool input, or one OpenAI computer_call action) and returns the screen after it, so you can drive a
session from your own computer-use loop:
screen = s.computer({"action": "left_click", "coordinate": [512, 300]}, max_width=1024) # Anthropic shape
s.computer({"type": "keypress", "keys": ["CTRL", "A"]}, screenshot=False) # OpenAI shape
# screen: {"ok", "text", "screenshot" (base64), "width", "height", "scale", "cursor", "url", "title"}
With max_width the screenshot is scaled down and the action's coordinates are read in its pixels: use the same
max_width on every call. A point outside the screen raises OutOfViewportError. Or let the agent do it:
bx.agent.run(task, mode="computer") (models with supportsComputerUse in bx.agent.models()); its steps carry the
model's thought, also streamed as thought events.
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, and origins limits it to fields on those sites (recommended for passwords).
run = 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"] == "tool":
print(event["name"], event.get("input")) # placeholders, never values
elif event["type"] == "done":
print(event["status"], event.get("result"))
agent.takeover(run_id) and agent.hand_back(run_id, note) hand the browser to a person and back; agent.wait(run_id)
polls until the run ends.
Limits, Continue and messages
A run stops at the first of its limits: max_steps (default 30; max_steps=None for none), max_cost_usd (optional,
model cost in USD), max_consecutive_errors tool errors in a row (default 5), the same call with the same result 5
times, or its session's time (timeout, for the run's own session). At a step, cost, error or no-progress limit it ends
with an errorCode (max_steps, max_cost, too_many_errors, no_progress), resultText says what is done and
what is left, and continuable["until"] says how long it can be continued: its session is kept for 10 minutes.
run = bx.agent.run("Turn every video in the workspace into 30-second clips", shell=True, max_steps=20, max_cost_usd=2)
done = bx.agent.wait(run["id"])
if done.get("continuable"):
print(done["resultText"]) # what is done, what is left
nxt = bx.agent.continue_run(done["id"], max_steps=30, instruction="The downloads are done; do the clips.")
done = bx.agent.wait(nxt["id"]) # nxt["continuedFrom"] == done["id"], same session
A run that had variables needs them again on continue_run (their values are never stored). NotContinuableError:
the run did not stop at a limit, was continued already, or its window passed.
Tell a working run something without taking the browser: it reads the message at its next step, and a run waiting for
your help (ask_user_for_help) takes it as the answer.
bx.agent.send_message(run["id"], "Also open page C and include its heading in the answer.")
Tasks and structured output
A task is a saved agent run: an instruction with %name% variables, an output schema, browser settings, a profile,
a model and, if you like, a schedule. Run it by hand or on its schedule; every run is an agent run.
task = bx.tasks.create(
"Books by category",
"Open https://books.toscrape.com, open the category %category% and return the first 3 books with their prices",
variables=[{"name": "category", "default": "Travel"}],
output={
"type": "object",
"properties": {
"category": {"type": "string"},
"books": {"type": "array", "items": {"type": "object", "properties": {"title": {"type": "string"}, "price": {"type": "number"}}, "required": ["title", "price"]}},
},
"required": ["category", "books"],
},
schedule={"cron": "0 9 * * MON-FRI", "timezone": "Europe/London", "enabled": False},
)
run = bx.tasks.run(task["id"], variables={"category": "Poetry"})
done = bx.tasks.wait_for_run(run) # done["result"]: the JSON answer (a dict)
print(done["status"], done["result"], done["resultText"])
for r in bx.tasks.runs(task["id"], status=["failed", "missed"]):
print(r["id"], r["errorCode"] or r["reason"])
bx.tasks.update(task["id"], schedule={"enabled": True}) # None removes a field, e.g. schedule=None
- Structured output works on one agent run too:
bx.agent.run(task, output=schema), thenbx.agent.wait(run_id).resultis the JSON answer andresultTexta one-sentence summary. An answer that still does not match after one repair try fails the run witherrorCode"output_invalid" (ErrorCode.OUTPUT_INVALID). - Variables: plain ones are written into the instruction (and kept with the run);
{"name", "secret": True, "origins"}is never stored, must come with every run, and is typed without the model seeing it. A task with a secret variable cannot have a schedule. A run missing a value raisesMissingVariablesError. - Credentials:
credentials=["SHOP"]ontasks.creategives every run the saved credentials' placeholders (see Credentials and browser profiles); a scheduled task may use them, and the task keeps the names only. - Schedules: five-field cron (at most every 5 minutes) read in
timezone. A scheduled run isqueueduntil it starts; a time that comes while a run is still going isskipped, and times the platform was down for aremissed(reason,missedCount). The plan limits tasks and schedules switched on (PlanLimitError). wait_for_runtakes the run fromrun()or(task_id, task_run_id), withpoll=andtimeout=in seconds; there is no GET for one task run, so it watches the task's unfinished runs.AsyncBoxlinehas the same methods.
Credentials and browser profiles
Credentials are write-only: the value is sealed when stored and never returned or shown. There are two types, a website
password (sites, user name, password and an optional 2FA key) and a secret (one value, such as an API token).
The name is the handle: the AI's placeholder (%GITHUB_TOKEN%, %SHOP.password%) and the shell variable
($GITHUB_TOKEN, $SHOP_PASSWORD). Each credential's scope says where it may be used: "agent" (the default: only
the AI), "shell" (only as variables in shells) or "all".
bx.credentials.create("GITHUB_TOKEN", "secret", value=os.environ["GITHUB_TOKEN"], scope="shell")
shop = bx.credentials.create(
"SHOP",
"password",
origins=["https://shop.example.com"], # the only site the AI may type it on
username="ops@example.com",
password=os.environ["SHOP_PASSWORD"],
totp_secret=os.environ.get("SHOP_2FA_KEY"), # optional: %SHOP.otp% is the current code
)
print(shop["type"], shop["username"], shop["hasTotp"]) # no value anywhere
# In a shell: $GITHUB_TOKEN, shown as %GITHUB_TOKEN% wherever it appears in the output.
s = bx.sessions.create(shell=True, env={"REGION": "eu"}, credentials=["GITHUB_TOKEN"])
s.exec("gh repo list --limit 3")
s.exec("./deploy.sh", credentials=["DEPLOY_KEY"]) # this one command only
# For the AI: typed only on the credential's sites, never shown to the model.
s.step("sign in with %SHOP.username% and %SHOP.password%", credentials=["SHOP"])
bx.agent.run("Sign in to https://shop.example.com with %SHOP.username% and %SHOP.password%", credentials=["SHOP"])
# Or type one field yourself without seeing it (the field's own frame must be on one of the credential's sites):
s.type_credential("SHOP", "password", "#password")
# A profile keeps cookies; link a password and the AI can sign in again when they expire.
bx.profiles.update(profile["id"], credential="SHOP")
for e in bx.credentials.audit(name="SHOP"):
print(e["at"], e["action"], e.get("actor"), (e.get("usedBy") or {}).get("type"))
- Exported credentials can be read by anything that runs in the shell, including an agent's commands that a web
page tries to steer. Export only what you accept that for; keep website passwords at scope
"agent"withorigins. Hiding values in output is a guard against accidents, not a boundary. A 2FA key never enters the machine:boxline-otp SHOPin the shell asks the platform for the current code. credentials.update(name, ...)changes the fields you pass; a new site needs the sensitive values again in the same call, and so does a change of where the codes come from (code_source,code_url: thepasswordagain).run_script(code, credentials=[...])lets the script'sstep()calls use credentials (the values never enter the machine). In a session with Chrome extensions, steps, scripts andtype_credentialneedallow_with_extensions=True, as agent runs with variables do.- Errors:
CredentialExistsError(useupdate),CredentialNotAllowedError(the scope does not allow that use),NotFoundError(credential_not_found),TooManyCredentialValuesError(the session hides as many values as it can: start a new one),FeatureNotInPlanError(a password needs the plan'sloginDetails),MachineTooOldError(during a deploy),PlanLimitError(beyond the plan'smaxCredentials),CredentialCodeTimeoutError(no code or link came in time),CredentialLinkWrongSiteError(a sign-in link not on the credential's sites),CredentialLoginFailedError(session.logincould not sign in;run_idis the run that tried),CodeUrlNotAllowedError(acode_urlthat is not a public HTTPS address). - The
credential.changedwebhook event says a credential was created, changed (or linked to a profile) or deleted, never a value.
Codes sent by email or SMS, and signing in in one call
A password's 2FA codes can come from an authenticator key (code_source="totp", what totp_secret alone means), from
your system ("push") or from an endpoint of yours ("url"). The site's email or SMS goes to you; the AI
never sees the code or a sign-in ("magic") link.
bx.credentials.create(
"SHOP", "password", origins=["https://shop.example.com"], username="ops@example.com",
password=os.environ["SHOP_PASSWORD"],
code_source="push", # or "url" with code_url="https://ops.example.com/boxline-codes"
code_timeout_seconds=300, # how long a run waits for a code (5 to 900)
)
# A run, an action or `boxline-otp SHOP` that needs a code waits for a fresh one. The webhook credential.code_needed
# ({credential, type: "code" | "link", sessionId, runId}) says when; then push what the site sent:
bx.credentials.push_code("SHOP", code="482913") # a 2FA code
bx.credentials.push_code("SHOP", link="https://shop.example.com/magic?t=…") # or a sign-in link
# Sign in in one call: a short browser-only run with this one credential (default: the session's profile's).
page = s.login("SHOP", url="https://shop.example.com/login") # {url, title, runId}
- A pushed code or link is used once, by a wait that began before it arrived, and kept sealed for 10 minutes. A link must
be on one of the credential's sites.
push_codeis not retried by the SDK (a second push is a second code). - With
code_source="url"the platform asks yourcode_urlevery 5 s while a run waits (a signed POST; check it withverify_webhookand thecodeUrlSecretthatcreateandupdatereturn once;credentials.rotate_code_url_secret(name)makes a new one). Answer{"code": ...}or{"link": ...}, or 204 for "not yet". - Without a code in time a step, action or
loginraisesCredentialCodeTimeoutError.
A script with useModel
run_script runs Playwright (JavaScript) code inside the session (it needs shell=True). page, context,
browser, env, require, and the plain-English helpers step(), extract() and useModel() are in scope.
code = """
useModel("claude-haiku-4-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" }); // this call only
console.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"])
CAPTCHAs
By default (captcha="ask") the platform notices a CAPTCHA that waits for a person and hands over: agent runs and
steps pause, session.data["attention"] says which one, 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.
with bx.sessions.create(captcha="ask") as s:
s.goto("https://example.com/signup")
if s.refresh().data["attention"]:
print("a person is needed:", s.live_url)
s.wait_for_human(timeout=300) # raises CaptchaTimeoutError if nobody solves it
A plain-English step that waits too long raises CaptchaTimeoutError (409 captcha_timeout).
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:
s = bx.sessions.create(block_ads=True, cookie_banners="reject") # "reject" is the default; "off" leaves banners alone
s.goto("https://example.com")
s.refresh()
print(s.blocked_requests) # refused inside the machine, before any proxy (collected about every 30 s)
s.update(block_ads=False, cookie_banners="off")
bx.fetch("https://example.com", block_ads=True) # screenshot, pdf, extract, crawl.start and agent.run take it too
Chrome extensions (Manifest V3, plan feature extensions): upload the zip of the extension's folder once, then start
sessions with it (at most 10, at start only).
ext = bx.extensions.upload("my-extension.zip") # a path, or the zip's bytes; at most 10 MB
s = bx.sessions.create(extensions=[ext["id"]]) # loaded before the session is returned
for 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 such as passwords too),
and can send them anywhere. Upload only extensions you trust, and keep secrets in sessions without extensions. An
agent run with
variablesin a session with extensions raisesVariablesWithExtensionsErrorunless you passallow_with_extensions=True. - The upload is checked before it is stored:
InvalidExtensionErrorsays why (not a zip, Manifest V2, a native binary, adebuggerornativeMessagingpermission…),PayloadTooLargeErrorover 10 MB,LimitReachedErrorbeyond 100 extensions,ExtensionDeniedErrorfor one the operator blocks. Uploads carry anIdempotency-Key, so a retry never stores one twice.
Webhooks
Signed HTTPS callbacks when a session ends, an agent run, task run or crawl finishes, or a CAPTCHA waits for a person.
endpoint = bx.webhooks.create("https://example.com/webhooks/boxline", ["session.ended", "agent_run.finished"])
# endpoint["secret"] ("whsec_…") is shown only now: store it with your other secrets.
In your receiver, verify every delivery against the raw body, and drop events you have handled already (a delivery can arrive more than once):
from boxline import verify_webhook, WebhookSignatureError
# body: the raw bytes exactly as received (request.body in Django, request.get_data() in Flask); re-serialised
# JSON will not match.
try:
event = verify_webhook(body, request.headers.get("Boxline-Signature"), os.environ["BOXLINE_WEBHOOK_SECRET"])
except WebhookSignatureError as e:
return Response(e.reason, status=400) # malformed, timestamp_out_of_range, no_matching_signature
if not already_handled(event["id"]): # dedupe by the id in the signed body
handle(event)
return Response(status=200)
- The signature's timestamp must be within 5 minutes of your clock (
tolerance_seconds=changes it), which stops replays of old deliveries. bx.webhooks.rotate_secret(id)returns a new secret; for 24 hours deliveries are signed with both, so switch at your pace.verify_webhooktakes a list too:[new_secret, old_secret].bx.webhooks.test(id)sends awebhook.testevent now;bx.webhooks.test(id, type="agent_run.waiting")sends a made-up sample of that type (markedtest: True).bx.webhooks.deliveries(id, status="failed")lists deliveries (newest first) with each attempt's status anderrorCode;retry_delivery(id, delivery_id)sends one again.bx.webhooks.event_types()lists every event type with a description;events=["*"]subscribes to all of them, also types added later, so ignore types you do not know.boxline.types.WebhookEventPayloadis the union of every event's shape, keyed bytype.webhook_signature_header(secret, body)signs a body the way the API does, for your receiver's tests.- Endpoints must be public HTTPS (
WebhookUrlNotAllowedError); a plan haswebhookEndpointsof them.
Async
AsyncBoxline has the same methods; await them, and use async for / async with:
import asyncio
from boxline import AsyncBoxline
async 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())
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):
page = bx.sessions.list(status=["RUNNING", "PAUSED"], limit=50)
print(len(page.data), page.total, page.next)
for s in bx.sessions.list(status="RUNNING"):
print(s.id)
for e in session.events(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. Lists: sessions.list, sessions.events, sessions.pages, profiles.list, api_keys.list,
agent.list, crawl.list, extensions.list, tasks.list, tasks.runs, credentials.list, credentials.audit, and a crawl's pages (crawl.get(id, after=…),
or for p in bx.crawl.pages(id)).
Errors, retries and time limits
- Typed errors. Every failure is a
BoxlineErrorwithstatus,code(stable, snake_case),message,request_id(the API's id: quote it when asking for support) andclient_request_id(yours, if you sent one). Subclasses:RateLimitError(retry_after),FeatureNotInPlanError,ProjectSuspendedError,IdempotencyMismatchError,IdempotencyInProgressError,InvalidCursorError,CaptchaTimeoutError,PageUnreachableError,PageTimeoutError,ModelRefusedError(extract),SearchUnavailableError,OutOfViewportError,WebhookUrlNotAllowedError,WebhooksUnavailableError,WebhookDisabledError,PayloadExpiredError,WebhookSignatureError(from verify_webhook),VariablesWithExtensionsError,InvalidExtensionError,PayloadTooLargeError,LimitReachedError,ExtensionDeniedError,CrossSiteRequestError,MissingVariablesError,PlanLimitError,CredentialExistsError,CredentialNotAllowedError,TooManyCredentialValuesError,CredentialCodeTimeoutError,CredentialLinkWrongSiteError,CredentialLoginFailedError,CodeUrlNotAllowedError,MachineTooOldError,NotContinuableError,TooManyMessagesError,SessionNotRunningError,AuthenticationError,NotFoundError, andBoxlineConnectionError/BoxlineTimeoutErrorwhen no answer came back.ErrorCodehas the codes. - Retries. GETs, and the calls that create or start something (sessions, bulk, agent runs, continued runs, messages
to runs, crawls, API keys, profiles, extension uploads, tasks, task runs), are retried after a network error, a time-out, 429 and 5xx: 2 retries by default, exponential backoff
from 0.5 s to 8 s with jitter, or what
Retry-After/RateLimit-Resetsay (up to 60 s; longer waits go to you as aRateLimitError). Other POSTs (exec, actions, fetch…) are never retried: they could run twice. - Idempotency keys. The SDK sends a new
Idempotency-Keywith every create, and the same one on its retries, so a retry never starts a second session or run. Pass your own to make your own retries safe:bx.sessions.create(options={"idempotency_key": "job-42"}). The same key with another body raisesIdempotencyMismatchError(for a new API key: also when another caller used it). - Time limits.
timeoutin seconds per request (default 120); long calls (exec,files.wait_for, plain-English steps) get the time they need.
bx = Boxline(max_retries=3, timeout=60)
bx.sessions.get(session_id, options={"timeout": 5, "max_retries": 0, "client_request_id": "trace-42"})
patient = bx.with_options(max_retries=6)
Every request carries Boxline-SDK: python/<version>.
Every method
Every public API operation has a method, with the Node SDK's names in snake_case (the full list is
docs/sdk-methods.json in the platform repository). Session methods take the id first; a Session has the same
methods without it (session.pause()).
| Area | Methods |
|---|---|
| Account | me, has_feature, auth.signup, auth.login, auth.logout, project.trajectories, project.set_trajectories, project.settings, project.set_settings, api_keys.list, api_keys.create, api_keys.revoke |
| Webhooks | webhooks.create, list, get, update, delete, rotate_secret, test, deliveries, retry_delivery; verify_webhook (no request) |
| Sessions | sessions.create, get, list, update, release, pause, resume, move, extend, rotate_proxy, rotate_urls, live, bulk |
| Browser | sessions.actions, sessions.computer; on a session: goto, click, hover, fill, type, type_credential, press, scroll, wait, select, elements, evaluate, content, screenshot, cursor, upload, tabs, new_tab, switch_tab, close_tab, back, forward, reload, step, extract, export_cookies, computer, mouse.move/move_by/click/down/up/drag, keyboard.key/type/press |
| Shell and scripts | sessions.exec, exec_stream, run_script, restart_shell |
| Files | sessions.files.list, read, read_text, write, delete, wait_for |
| Logs | sessions.events, stream_events, pages, recording, recording_frame; on a session: wait_for_human |
| Browser profiles | profiles.create, get, list, update, delete |
| Credentials | credentials.create, list, get, update, delete, audit, push_code, rotate_code_url_secret; on a session: type_credential, login |
| Extensions | extensions.upload, list, get, delete |
| Web | fetch, screenshot, pdf, extract, search, crawl.start, get, list, cancel, pages, wait |
| Agent | agent.models, run, get, list, takeover, hand_back, cancel, continue_run, send_message, stream, wait |
| Tasks | tasks.create, list, get, update, delete, run, runs, wait_for_run |
| Usage | usage, stats, pricing, openapi, health |
Examples
In examples/. Most use the small example site (python examples/test_site.py), which only a local API's sessions can
reach, so run them with BOXLINE_API_URL=http://localhost:8080; search.py and quickstart.py work anywhere.
| File | Shows |
|---|---|
quickstart.py |
a session, Playwright over CDP, the actions API |
mixed_download_process_fill.py |
Playwright download → Python in the shell → files API → a form filled with actions |
agent_mixed.py |
one agent run using the browser and the shell, streamed step by step |
script_mixed.py |
a script mixing Playwright, child_process, fs, useModel and step() |
async_quickstart.py |
AsyncBoxline, asyncio.gather, async for |
page_apis.py |
fetch, screenshot, pdf, crawl, extract, a page that cannot load |
search.py |
web search, with the top pages fetched as Markdown, and the cache |
computer_use.py |
mouse and keyboard (drag, hover, double-click, keys), each action's text, session.computer() in both shapes |
agent_computer.py |
an agent run in computer mode, with its thoughts streamed |
webhook_receiver.py |
a receiver (standard library only) that verifies each delivery and drops duplicates |
webhooks.py |
endpoints, deliveries and their history, re-sending, rotating the secret, a test event |
agent_variables.py |
an agent run with %email% / %password% limited to one site |
tasks.py |
a task with an output schema on a demo shop: run with a variable, waited for, its history, changed, deleted; an agent run with output |
credentials.py |
a secret exported into a shell (its length checked, its value hidden in output), changed, audited, deleted; a password with 2FA linked to a profile |
script_use_model.py |
useModel, step() and extract() in a script |
captcha.py |
noticing a CAPTCHA and handing it to a person |
block_ads.py |
a session that blocks ads and trackers, and its blocked_requests count |
extension.py |
a tiny MV3 extension built with zipfile, uploaded, loaded into a session, and deleted |
list_sessions.py |
cursor pages and iteration |
reliability.py |
idempotency keys, request ids, typed errors, time limits |
Developing the SDK
src/boxline/_async_client.py is the source of both clients; _client.py is generated from it:
uv venv .venv && uv pip install --python .venv/bin/python -e ".[dev]" # the distribution is boxline-sdk
python scripts/unasync.py # after editing _async_client.py
.venv/bin/python -m pytest # unit tests (fake HTTP), coverage of docs/openapi.yaml, the generated file
This repository holds the Boxline Python SDK (boxline-sdk). It is copied from Boxline's main repository on every change. Issues and pull requests are welcome here; accepted changes are made there and arrive with the next copy.
Metadata
Release files for boxline-sdk 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| boxline_sdk-1.3.0.tar.gz | 118.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| boxline_sdk-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 222.0 kB
Release files / boxline_sdk-1.3.0.tar.gz
| Download URL | boxline_sdk-1.3.0.tar.gz |
|---|---|
| Size | 118.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
23081fd07e6c92893ed4e697b05e8a4c204a18765756393065f0c267e779c7a3
|
|
BLAKE2b-256 checksum How to use checksums |
33062de99adc3149fd47d530cb48124f922cd9886f6a6d567d2f0f0a89e99bcd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / boxline_sdk-1.3.0-py3-none-any.whl
| Download URL | boxline_sdk-1.3.0-py3-none-any.whl |
|---|---|
| Size | 103.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cbabbd8dcd2d1cb5a98723b58e2d396db27830bca147e9de9499db4301a50f54
|
|
BLAKE2b-256 checksum How to use checksums |
2702a0c2a06e47e3f2c583c2c7732d7fb93b760ab3ecc7aa6d36f19150aeda51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log