Hyperbrowser Python SDK
Checkout the full documentation here
Installation
Currently Hyperbrowser supports creating a browser session in two ways:
- Async Client
- Sync Client
It can be installed from pypi by running :
pip install hyperbrowser
The browser-control examples below also use Playwright:
pip install playwright
Configuration
Both the sync and async client follow similar configuration params
API Key
The API key can be configured either from the constructor arguments or environment variables using HYPERBROWSER_API_KEY
If no API key is provided, the client falls back to a saved OAuth session created by hx auth login. By default it reads ~/.hx_config/auth/default.json, or ~/.hx_config/auth/<profile>.json when HYPERBROWSER_PROFILE or ClientConfig(profile=...) is set.
Profile names must match ^[A-Za-z0-9._-]+$.
base_url and HYPERBROWSER_BASE_URL accept either https://host or https://host/api. The client normalizes both to the same control-plane base URL.
WebMCP page tools
Create a session with {"enable_web_mcp": True} (default: false), then navigate
to a page that exposes WebMCP tools using Playwright. With that session open:
discovery = client.sessions.webmcp.list_tools(session.id)
# Select a tool using its name, source page/frame, and input schema.
tool = next(tool for tool in discovery.tools if tool.name == "search")
result = client.sessions.webmcp.invoke(session.id, {
"tool_ref": tool.tool_ref,
"input": {"query": "flights to Tokyo"}, # Match the selected tool's schema.
})
print(result.status, result.output)
For long-running tools or forms awaiting human submission, use
client.sessions.webmcp.start(session_id, params), then
client.sessions.webmcp.get_result(session_id, invocation_id, {"wait_seconds": 30})
until the status is terminal. client.sessions.webmcp.cancel(session_id, invocation_id)
requests best-effort cancellation. Blocking invocation defaults to 60 seconds
(maximum 120); start defaults to 300 seconds (maximum 3,600) via timeout_seconds.
All five methods also work with AsyncHyperbrowser by adding await.
Discover tools again after navigation. Invocations never automatically retry;
losing a response can leave an unknown outcome. Handles are ephemeral and
terminal results expire after 10 minutes or earlier eviction. Check result
status and truncation metadata before using output. Tool input and output keys
are not renamed. Request TypedDicts are available in hyperbrowser.types, and
Pydantic request/response models in hyperbrowser.models.
See the WebMCP guide for complete examples, cancellation behavior, human submission, and limits.
Web Bot Auth
A session can sign its requests with Web Bot Auth so websites can verify that they come from Hyperbrowser. Web Bot Auth is enabled for teams on request.
session = client.sessions.create({"web_bot_auth": True})
print(session.launch_state.web_bot_auth.signature_agent) # https://www.hyperbrowser.ai
# Sign only requests to these hosts and their subdomains:
client.sessions.create({"web_bot_auth": {"only_domains": ["example.com"]}})
Agent tasks accept web_bot_auth in session_options; scrape, crawl, and extract
jobs do not. It cannot be combined with use_ultra_stealth or
start_from_snapshot. See the
Web Bot Auth guide.
Proxy routes
proxy_routes sends some hosts' traffic straight out instead of through the
session's proxy, or the other way round. The first route that matches a host
decides; hosts that no route matches use the proxy. Proxy routes require
use_proxy and are enabled for teams on request.
session = client.sessions.create({
"use_proxy": True,
"proxy_routes": [
{"match": "api.example.com", "proxy": "default"}, # stays on the proxy
{"match": ["*.example.com", "cdn.example.net"], "proxy": "direct"},
],
})
A pattern is a hostname, *.example.com (its subdomains, not example.com
itself), or * (every host). Proxy routes only apply to client.sessions.create
and cannot be combined with allow_internet_access=False, allow_out, or
deny_out. See the
proxy routes guide.
Fill credentials into browser sessions
Any secret with allowed origins can be typed into pages of a browser session. A website login is usually two such secrets grouped in a vault, plus a TOTP secret when the site asks for a one-time code. Values are encrypted on your machine; after upload, neither the API, your code, nor an LLM sees them again.
from playwright.sync_api import sync_playwright
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
origins = ["https://github.com"]
client.secrets.create(
{"name": "github-username", "value": "ada@example.com", "allowed_origins": origins}
)
client.secrets.create(
{"name": "github-password", "value": "correct horse", "allowed_origins": origins}
)
# A TOTP seed: base32 as the site shows it, or an otpauth://totp/ URI.
client.secrets.create(
{
"name": "github-otp",
"type": "totp",
"value": "JBSW Y3DP EHPK 3PXP JBSW Y3DP EHPK 3PXP",
"allowed_origins": origins,
}
)
client.vaults.create(
{
"name": "github-login",
"entries": {
"USERNAME": {"name": "github-username"},
"PASSWORD": {"name": "github-password"},
"OTP": {"name": "github-otp"},
},
}
)
# Secrets and vaults can only be attached when the session is created.
session = client.sessions.create({"credentials": [{"vault": "github-login"}]})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.ws_endpoint)
page = browser.contexts[0].pages[0]
page.goto("https://github.com/login")
# With a vault, each field names the entry to type.
result = client.sessions.credentials.fill(
session.id,
{
"vault": "github-login",
"fields": [
{"field": "USERNAME", "selector": "#login_field"},
{"field": "PASSWORD", "selector": "#password"},
],
},
)
if all(field.outcome == "filled" for field in result.fields):
page.click("input[type=submit]") # Fill only types; submitting is up to you.
page.wait_for_selector("#app_totp")
# A TOTP entry types the code that is current at fill time.
client.sessions.credentials.fill(
session.id,
{"vault": "github-login", "fields": [{"field": "OTP", "selector": "#app_totp"}]},
)
client.sessions.stop(session.id)
A single secret works without a vault: attach it by name ("credentials": ["github-password"] or [{"secret": "github-password"}]) and fill it with
{"credential": "github-password", "fields": [{"selector": "#password"}]}.
fill() returns the filled page's page_id and origin, the version for a
single-secret fill, and per input its selector, outcome, secret_id,
version, field (vault fills), reference (1Password fills) and
expires_at (TOTP codes). Values are never returned.
| Outcome | Meaning |
|---|---|
filled |
The input now holds the value. |
not_found |
No input matched the selector. |
ambiguous |
Several visible inputs matched the selector. |
not_editable |
The input is disabled, read-only, hidden, or not a text input. |
not_accepted |
The page didn't keep the value, for example because of maxlength. |
Things to know:
- A secret can be filled only on its allowed origins (
scheme://host[:port];https://*.example.commatches any subdomain but notexample.comitself). A secret takes 1-20 of at most 512 characters, https only (http forlocalhost,127.0.0.1and[::1]), with no path and*.only on a registrable domain such asexample.com, never*.comor an IP address. The API validates and normalizes them. Change them withclient.secrets.set_allowed_origins("github-password", [...]);Noneclears them, after which it can't be filled.client.secrets.update()takes{"metadata": {"browser": None}}too: each metadata member sent replaces the stored one andNoneremoves it. - Secret types are
secret(the default),totpandop_token, fixed at creation;client.secrets.list({"type": "totp"})filters by type. TOTP values are checked locally before upload: a base32 seed (spaces, hyphens, line breaks, lowercase and trailing=are fine) or anotpauth://totp/URI withalgorithmSHA1/SHA256/SHA512,digits6 or 8 andperiod1-300. Seeds shorter than 10 bytes (16 characters) are accepted but weak. The platform can't read values, so a bad seed would otherwise only fail when used. A TOTP secret delivered to a sandbox holds the code current at process start. - A fill takes one of
credential,vaultandone_password, and 1-16 fields with unique selectors;fieldis required with a vault and left out without one, andrefis required withone_passwordand left out without it. Withoutpage_id, fill considers the tabs whose URL starts withpage_url(all tabs when it's not given). If one is left, that's the target. If several are left, it prefers the tabs on an origin every secret allows (skipped withframe_url, since a login frame's tab is usually on another origin), then the visible one among them; if more than one still qualifies, it fails withCREDENTIAL_PAGE_AMBIGUOUS. The chosen tab's origin is then checked: on any other origin nothing is typed and the fill fails withCREDENTIAL_ORIGIN_NOT_ALLOWED. Under Playwright every tab reports as visible, so passpage_url(a URL prefix) or the tab's CDP target ID aspage_id:page.context.new_cdp_session(page).send("Target.getTargetInfo")["targetInfo"]["targetId"]. For a login form in an iframe, passframe_url(a URL prefix). - Fill is safe to retry and never retried automatically. It waits at least 30 seconds for the API (the client timeout when longer).
- Attaching secrets needs an API key with
secrets.use; filling also needsbrowser.sessions.update. The team needs the secrets feature. Sessions with credentials run as Firecracker browsers with Chrome's password manager off. A vault attaches the secrets its entries hold when the session is created. - Fills are recorded in the session's event log as
credential_filledandcredential_fill_refused. Their metadata names thecredentialorvault; for 1Password fills,onePasswordToken(the op_token's name) and itssecretId. - Errors are raised as
HyperbrowserErrorwith the API'scode,retryable, andstatus_code.detailsholds the API's response body, so the candidate tabs of an ambiguous fill are inerror.details["details"]["pages"](each withpageId,urlandtitle). Error codes from the API:CREDENTIAL_PAGE_AMBIGUOUS(409): several tabs could be the target; pass one'spageIdaspage_id, or apage_url.CREDENTIAL_FRAME_AMBIGUOUS(409) is the same for frames.CREDENTIAL_ORIGIN_NOT_ALLOWED(403): the page's origin (indetails) isn't allowed; nothing was typed.CREDENTIAL_NOT_ATTACHED(403): the secret (or a 1Password fill's op_token) wasn't attached when the session was created, or a vault entry now points at another secret.CREDENTIAL_NOT_READY(409): the secret has no allowed origins or no value.SECRET_TYPE_MISMATCH(409): anop_tokencan't be attached or filled as a secret (by name,{"secret": ...}or a vault entry); attach it with{"one_password": ...}instead. Also another type named as one.SECRET_VALUE_INVALID(422): the stored value is unusable, such as a bad TOTP seed; add a new version.CREDENTIAL_PAGE_NOT_FOUND,CREDENTIAL_FRAME_NOT_FOUND,SECRET_NOT_FOUND,SECRET_VERSION_NOT_FOUND,VAULT_NOT_FOUND,VAULT_ENTRY_NOT_FOUND(404),SESSION_NOT_ACTIVE(409).CREDENTIAL_FILL_TIMEOUT(503/504, retryable) andCREDENTIAL_FILL_FAILED(500/502/503, retryable as flagged).CREDENTIALS_UNSUPPORTED_RUNTIME(400),CREDENTIAL_FILL_UNSUPPORTED(501) andCREDENTIAL_VALUE_UNSUPPORTED(501): the session's browser can't fill this, such as 1Password fields on an older host.SECRET_ONEPASSWORD_REFERENCE_FAILED(422): a 1Password item or field couldn't be read, so nothing was filled.detailshas the failingselectorsandreasonsby selector.SECRET_ONEPASSWORD_AUTH_FAILED(422): 1Password rejected the token, or the token is malformed or doesn't sign in to1password.com,.caor.eu(or a subdomain), so it wasn't sent to 1Password.SECRET_ONEPASSWORD_RATE_LIMITED(429) andSECRET_ONEPASSWORD_FAILED(502/503) are retryable. Each has the token'stokenSecretIdandtokenVersionindetails.- Local fill shape errors raise
invalid_request; invalid secret values raiseinvalid_secret_valuebefore upload. Fill, origin, vault-entry and 1Password policy errors come from the API. Inspect the status, code (when supplied) and details; schema errors may have no machine code.
Fill from 1Password
A fill can also type fields of a 1Password vault, read with a 1Password
service account token stored as an op_token secret (see
Manage secrets). Attach
the token when creating the session, then name the vault, the origins the
values may be typed on, and each input's field:
session = client.sessions.create({"credentials": [{"one_password": "op-token"}]})
# ... open https://www.scrapingcourse.com/login in the session ...
result = client.sessions.credentials.fill(
session.id,
{
"one_password": {
"token": "op-token", # the op_token secret; its latest version signs in
"vault": "Dev Vault", # the 1Password vault's name or ID
# Nothing stored says where a 1Password value belongs, so the
# request names the origins it may be typed on.
"allowed_origins": ["https://www.scrapingcourse.com"],
},
"fields": [
{"selector": "#email", "ref": {"item": "scrapingcourse login", "field": "username"}},
{"selector": "#password", "ref": {"item": "scrapingcourse login", "field": "password"}},
{
"selector": "#otp",
"ref": {"item": "scrapingcourse login", "section": "Security", "field": "otp?attribute=totp"},
},
],
"page_url": "https://www.scrapingcourse.com/login",
},
)
for field in result.fields:
print(field.selector, field.outcome, field.reference) # op://Dev Vault/...
tokennames the op_token secret, which must be attached to the session ({"one_password": name}counts toward the 20 credentials). An op_token is never typed itself, and attaching or filling it as a plain secret fails withSECRET_TYPE_MISMATCH.allowed_originstakes 1-20 origins with the same rules as a secret's. A page on another origin fails withCREDENTIAL_ORIGIN_NOT_ALLOWED, as for stored secrets.refis{"item", "section"?, "field"}by name or ID, with the same rules as a sandbox'sone_passwordfields: a field may end with a query such as?attribute=otp, andop://<vault>/<item>/[<section>/]<field>is at most 512 bytes. Every field takesrefand none takesfield.- The result has no top-level
version; each input'ssecret_idandversionare the op_token's, andreferenceis theop://reference it was typed from. - The API validates these rules. Request models still reject unknown members and invalid shapes locally.
Use with LLM SDKs
CredentialFillTool lets a model fill a secret or vault by name while your
application binds the session ID. It doesn't offer 1Password fills: their
request names the allowed origins, which a model shouldn't choose, so call
client.sessions.credentials.fill() with one_password directly. The model chooses only credential or
vault, the fields (selector, and field with a vault), and optionally
page_id, page_url or frame_url; it never sees the values. runnable()
returns JSON text for the model: the fill result, or {"error": {"code", "message", "retryable", "details"}} when the fill fails, so the model can
react (for example by choosing a page_id). Give the model your other
browser tools too, since it must submit the form itself. async_runnable()
does the same with AsyncHyperbrowser.
OpenAI (Chat Completions):
import json
from openai import OpenAI
from hyperbrowser.tools import CredentialFillTool
openai_client = OpenAI()
tools = [CredentialFillTool.openai_tool_definition] # plus your browser tools
messages = [{"role": "user", "content": "Sign in to GitHub with the vault named github-login."}]
while True:
response = openai_client.chat.completions.create(
model="gpt-5", messages=messages, tools=tools
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
break
for call in message.tool_calls:
if call.function.name == "fill_credential":
output = CredentialFillTool.runnable(
client, session.id, json.loads(call.function.arguments)
)
else:
output = run_browser_tool(call) # your click/press/navigate tools
messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
Anthropic (Messages):
import anthropic
from hyperbrowser.tools import CredentialFillTool
anthropic_client = anthropic.Anthropic()
tools = [CredentialFillTool.anthropic_tool_definition] # plus your browser tools
messages = [{"role": "user", "content": "Sign in to GitHub with the vault named github-login."}]
while True:
response = anthropic_client.messages.create(
model="claude-sonnet-5-5", max_tokens=1024, tools=tools, messages=messages
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type != "tool_use":
continue
if block.name == "fill_credential":
output = CredentialFillTool.runnable(client, session.id, block.input)
else:
output = run_browser_tool(block)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
Gemini (google-genai):
from google import genai
from google.genai import types
from hyperbrowser.tools import CredentialFillTool
gemini = genai.Client()
config = types.GenerateContentConfig(
tools=[
types.Tool(function_declarations=[CredentialFillTool.gemini_tool_definition])
] # plus your browser tools
)
contents = [
types.Content(
role="user",
parts=[types.Part.from_text(text="Sign in to GitHub with the vault named github-login.")],
)
]
while True:
response = gemini.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config
)
contents.append(response.candidates[0].content)
if not response.function_calls:
break
parts = []
for call in response.function_calls:
if call.name == "fill_credential":
output = CredentialFillTool.runnable(client, session.id, call.args or {})
else:
output = run_browser_tool(call)
parts.append(
types.Part.from_function_response(name=call.name, response={"result": output})
)
contents.append(types.Content(role="user", parts=parts))
Tools for LLM function calling
hyperbrowser.tools has ready-made tools: WebsiteScrapeTool,
WebsiteScreenshotTool, WebsiteCrawlTool, WebsiteExtractTool,
BrowserUseTool and CredentialFillTool. Each has openai_tool_definition
(a strict Chat Completions function tool), anthropic_tool_definition, and
runnable() / async_runnable() to run the model's input and return text for
the model. CredentialFillTool also provides gemini_tool_definition, a
function declaration dict for google-genai.
Usage
Hyperbrowser 1.0 accepts plain dictionaries for request parameters. Method
signatures use TypedDict definitions, so editors can autocomplete keys at
every nested level:
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
# Preferred in 1.0: autocomplete works directly in the dictionary.
session = client.sessions.create(
{
"use_stealth": True,
"screen": {"width": 1920, "height": 1080},
}
)
Browser sessions can also use an outbound network policy. Omitting these fields keeps the default unrestricted behavior:
session = client.sessions.create(
{
"allow_internet_access": False,
"allow_out": ["example.com"],
"deny_out": ["0.0.0.0/0"],
}
)
Direct browser policies accept domains, IPv4 addresses, and CIDR ranges in
allow_out; deny_out accepts IPv4 addresses and CIDR ranges. With a proxy,
allow rules must be domains and the only supported deny rule is 0.0.0.0/0.
Existing Pydantic request classes remain accepted, so upgrading does not require an immediate rewrite:
from hyperbrowser.models import CreateSessionParams, ScreenConfig
session = client.sessions.create(
CreateSessionParams(
use_stealth=True,
screen=ScreenConfig(width=1920, height=1080),
)
)
Import request annotations from hyperbrowser.types when a named variable is
useful. The same names under hyperbrowser.models refer to the legacy Pydantic
request classes. Responses remain Pydantic models.
JSON Schema fields accept raw schema values, including object schemas with
$defs, $ref, or custom keywords, and boolean schemas where the API supports
them. Those schemas and other user-owned mappings are preserved as data; only
SDK-owned request keys are translated to their API aliases. Schema fields
documented as accepting a model class can also generate a schema from a Pydantic
model.
See the Hyperbrowser Python SDK 1.0 migration guide for the complete compatibility details and migration checklist.
Async
import asyncio
from hyperbrowser import AsyncHyperbrowser
from playwright.async_api import async_playwright
HYPERBROWSER_API_KEY = "test-key"
async def main():
async with AsyncHyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
session = await client.sessions.create()
try:
async with async_playwright() as playwright:
browser = await playwright.chromium.connect_over_cdp(
session.ws_endpoint
)
context = browser.contexts[0]
page = context.pages[0]
print("Navigating to Hacker News...")
await page.goto("https://news.ycombinator.com/")
print("Page title:", await page.title())
finally:
await client.sessions.stop(session.id)
# Run the asyncio event loop
asyncio.run(main())
Sync
from playwright.sync_api import sync_playwright
from hyperbrowser import Hyperbrowser
HYPERBROWSER_API_KEY = "test-key"
def main():
client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
session = client.sessions.create()
ws_endpoint = session.ws_endpoint
# Launch Playwright and connect to the remote browser
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(ws_endpoint)
context = browser.new_context()
# Get the first page or create a new one
if len(context.pages) == 0:
page = context.new_page()
else:
page = context.pages[0]
# Navigate to a website
print("Navigating to Hacker News...")
page.goto("https://news.ycombinator.com/")
page_title = page.title()
print("Page title:", page_title)
page.close()
browser.close()
print("Session completed!")
client.sessions.stop(session.id)
# Run the asyncio event loop
main()
Sandboxes
The sync and async clients expose the same sandbox APIs through client.sandboxes.
Create a sandbox with pre-exposed ports
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create(
{
"image_name": "node",
"cpu": 2,
"memory_mib": 2048,
"disk_mib": 8192,
"exposed_ports": [{"port": 3000, "auth": True}],
}
)
print(sandbox.exposed_ports[0].browser_url)
print(sandbox.cpu, sandbox.memory_mib, sandbox.disk_mib)
sandbox.stop()
client.close()
cpu, memory_mib, and disk_mib are only supported for image launches.
Manage volumes and mount them in a sandbox
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
volume = client.volumes.create({"name": "project-cache"})
all_volumes = client.volumes.list()
same_volume = client.volumes.get(volume.id)
sandbox = client.sandboxes.create(
{
"image_name": "node",
"mounts": {
"/workspace/cache": {
"id": same_volume.id,
"type": "rw",
"shared": True,
}
},
}
)
sandbox.stop()
client.volumes.delete(same_volume.id)
client.close()
Manage secrets and pass them to sandbox processes
Secrets hold values such as API keys for sandbox processes. Values are encrypted on your machine to the platform's public key before upload, and the API never returns them. Methods that take a secret accept its ID or exact name.
from pathlib import Path
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
# Values are str or bytes and are stored byte-for-byte, trailing newlines included.
client.secrets.create(
{
"name": "provider-key",
"description": "Provider API key",
"value": Path("provider-key.txt").read_bytes(),
}
)
client.secrets.add_version("provider-key", "rotated-value") # becomes version 2
detail = client.secrets.get("provider-key") # metadata and versions, never the value
client.secrets.list({"search": "provider"})
# A vault groups variables; sandboxes and processes take it by name.
client.vaults.create(
{"name": "app-env", "entries": {"API_KEY": {"name": "provider-key"}}}
)
sandbox = client.sandboxes.create(
{
"image_name": "node",
# Delivered to the image's startup command (image launches only).
"vault": "app-env",
}
)
print(sandbox.startup_secrets) # what each variable received, never values
# A plain name uses the latest version; {"name": ..., "version": N} pins one.
# `secrets` are merged over the vault's entries and win on a clash.
result = sandbox.exec(
"node app.js",
vault="app-env",
secrets={"API_KEY": {"name": "provider-key", "version": 2}},
)
process = sandbox.processes.start("node worker.js", secrets={"API_KEY": "provider-key"})
print(process.secrets) # the version each variable received
client.vaults.delete("app-env")
client.secrets.delete_version("provider-key", 1)
client.secrets.delete("provider-key")
client.close()
Values can also come straight from a 1Password vault. Store a 1Password
service account token (it starts with ops_) as an op_token secret: it is
only used to sign in to 1Password, never set as a variable or typed into a
browser itself, and can't have allowed origins. Browser sessions can fill
1Password fields too; see Fill from 1Password. Then name the fields each variable
takes with one_password when creating a sandbox or starting a process.
Nothing about them is stored: the sandbox reads the fields when the process
starts.
client.secrets.create(
{"name": "op-token", "type": "op_token", "value": Path("op-token.txt").read_text().strip()}
)
one_password = {
"token": "op-token", # the op_token secret's name; its latest version signs in
"vault": "Engineering", # the 1Password vault's name or ID
# Items, sections and fields by name or ID: op://Engineering/Postgres/prod/password
"fields": {
"DB_PASS": {"item": "Postgres", "section": "prod", "field": "password"},
"DB_OTP": {"item": "Postgres", "field": "one-time password?attribute=totp"},
},
}
# Delivered to the image's startup command (image launches only).
sandbox = client.sandboxes.create({"image_name": "node", "one_password": one_password})
print(sandbox.startup_secrets) # each with its op:// reference and token_secret
result = sandbox.exec("psql -c 'select 1'", one_password=one_password)
process = sandbox.processes.start("node worker.js", one_password=one_password)
print(process.secrets) # source.type is "onePassword" for these variables
one_password takes one token and vault per request and 1-32 fields, named
like environment variables. A field may end with a query such as
?attribute=otp, ?attribute=totp or ?ssh-format=openssh. Vaults, items,
sections and fields are 1-256 characters (UTF-16 code units) without /, ? or control characters, and
not . or ..; the whole op:// reference is at most 512 bytes. These are
checked by the API, as is a variable that secrets, env or the request's
vault also sets: a variable from 1Password must come from it alone.
Things to know:
- A secret is set only in the environment of the process it was passed to. Anything else running as the same user, or as root, in the sandbox can read that environment.
- A sandbox that has received secrets, and snapshots taken of it, report
contains_secrets=True. Restoring such a snapshot needs the same access as passing secrets. - Deleting a secret or version doesn't revoke a value already delivered to a sandbox.
- Limits: 100 secrets per team, 10 versions per secret, 32 secrets per process,
and values of 1 byte to 32 KiB of UTF-8 without NUL bytes. Names are 1-128
letters, digits,
_or-, starting with a letter or digit. - Reading secrets needs
secrets.read; delivering them to a sandbox needssecrets.use. Creating a secret needssecrets.manage. Updating, rotating, and deleting secrets or versions through the SDK need bothsecrets.readandsecrets.manage, since it resolves IDs and names through metadata reads.add_version()also reads the secret'sencryptionContextto encrypt its value. The team needs the secrets feature. - Secret-started process handles wait indefinitely by default with
wait()orresult(). A positivewait(timeout_ms=...)orwait(timeout_sec=...)limits the total local wait and raisesTimeoutError(asyncio.TimeoutErrorfor async handles); the process continues and the handle can be used to wait again. The start's timeout limits the command's execution separately. The sync client checks the limit between requests and caps each request's HTTP timeouts at the time left, so a slow response or a runtime token refresh can overrun it; use the async client for a strict limit. Secret process output comes from the receiver's retained output, somax_output_bytesdoes not apply; incomplete retained output raises an error. - A process start with secrets that times out, or whose result the API can't
confirm, raises a non-retryable error with code
secret_process_outcome_unknown: the command may be running, so checksandbox.processes.list()before retrying. When the API reported the unknown outcome itself, its response body, including its own code (SECRET_PROCESS_OUTCOME_UNKNOWN), is in the error'sdetails. client.sandboxes.create()withsecrets,vaultorone_passwordraises the samesecret_process_outcome_unknownerror when the create may have reached the API but its result is unknown (a timeout, a dropped connection, or an unreadable response): a sandbox may be running its startup command with the secrets, so list sandboxes before retrying.- Vaults hold up to 32 stored secrets, named like environment variables; a
team has up to 100. Methods that take a vault accept its ID or exact name,
and vaults need the same scopes as secrets. A vault,
secretsandone_passwordtogether deliver at most 32 variables, and a process'senvmay not set one of them. - Error codes from the API:
SECRETS_ADMIN_REQUIRED(403): the request used anhx auth loginsession of a team member who isn't an owner or admin. API keys are governed by their scopes instead.SECRETS_FEATURE_DISABLED(403): the team doesn't have the secrets feature.SECRETS_NOT_CONFIGURED(503): the platform has no secrets key configured. It isn't retried.SECRET_STARTUP_UNSUPPORTED(400): the image has no startup command to deliversecrets, avaultorone_passwordto. Create the sandbox without them, then pass them tosandbox.exec()orsandbox.processes.start().VAULT_NOT_FOUND(404),VAULT_NAME_TAKEN,VAULT_LIMIT_REACHED,VAULT_ENTRY_LIMIT_REACHED(409).SECRET_IN_USE(409): a vault uses the secret as an entry;SECRET_VERSION_IN_USE(409): a vault pins the version. Only vaults hold secrets, so anop_tokencan always be deleted.SECRET_TYPE_MISMATCH(409): anop_tokenused as a variable or vault entry, or another type named asone_password'stoken.SECRET_SOURCES_INVALID(400): for exampleenvsets a variable thatvaultorsecretsalso sets, aone_passwordvariable is also set byvault,secretsorenv, or the sources deliver more than 32 variables.SECRET_VALUE_INVALID(422): a stored value is unusable, such as a bad TOTP seed; fix it by adding a new version.SECRET_TOTP_UNSUPPORTEDandSECRET_ONEPASSWORD_UNSUPPORTED(501): the sandbox's host predates TOTP or 1Password, so nothing started. Retryable on sandbox create, which may land on a newer host; not on a process start.SECRET_ONEPASSWORD_REFERENCE_FAILED(422,detailsnames the variables) andSECRET_ONEPASSWORD_AUTH_FAILED(422, also a malformed token, or one that doesn't sign in to1password.com,.caor.eu, which is never sent to 1Password): fix the fields or the token.SECRET_ONEPASSWORD_RATE_LIMITED(429) andSECRET_ONEPASSWORD_FAILED(502/503) are retryable.retryableis the API's flag when it sends one.
List sandboxes with filters
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
result = client.sandboxes.list(
{
"status": "active",
"search": "sandbox",
"start": 1711929600000,
"end": 1712016000000,
"limit": 20,
}
)
for sandbox in result.sandboxes:
print(sandbox.id, sandbox.status)
List snapshots for a specific image
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
snapshots = client.sandboxes.list_snapshots(
{"image_name": "node", "status": "created", "limit": 10}
)
Expose and unexpose ports
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create(
{"image_name": "node", "cpu": 2, "memory_mib": 2048, "disk_mib": 8192}
)
result = sandbox.expose({"port": 8080, "auth": True})
print(result.url, result.browser_url)
sandbox.unexpose(8080)
Batch file writes with per-file options
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create({"image_name": "node"})
sandbox.files.write(
[
{
"path": "/tmp/config.json",
"data": '{"debug":true}\n',
"append": True,
"mode": "600",
},
{"path": "/tmp/blob.bin", "data": b"\x00\x01\x02"},
]
)
Run commands and collect output
sandbox.exec() and sandbox.processes.start() stream command output from the
receiver as soon as execution starts. The SDK collects stdout and stderr in
memory, so a completed result is not limited to the receiver's replay buffer.
This requires a receiver supporting streaming POST /sandbox/processes; roll
out the receiver before upgrading the SDK.
result = sandbox.exec("make test", max_output_bytes=128 * 1024 * 1024)
print(result.stdout, result.stderr, result.exit_code)
process = sandbox.processes.start("make test")
try:
for event in process.stream():
if event.type == "stdout":
print(event.data, end="")
result = process.wait()
finally:
process.disconnect()
The combined output limit defaults to 64 MiB per command and can be adjusted
with max_output_bytes. Exceeding it raises output_limit_exceeded. A broken
stream, missing output, or receiver truncation raises incomplete_output.
These errors include the process ID and do not automatically rerun the command.
Process streams use a separate 60-second read-idle timeout once response headers arrive. Output and the receiver's 15-second heartbeats reset this timeout, so quiet commands can run longer than the client's ordinary HTTP timeout. That ordinary timeout still applies to connection setup and waiting for response headers.
start() returns after the process starts and collects in the background.
wait(timeout_sec=...) limits the local wait; collection continues after a wait
timeout. The timeout passed to start() or exec() limits command execution.
A local wait timeout raises TimeoutError (asyncio.TimeoutError in the async API).
disconnect() stops collection and leaves the command running. Use kill() to
stop it. Reattaching with get() can retrieve only retained receiver output;
wait() raises if that output has been truncated.
The async API has the same behavior: await exec(), start(), wait(), and
disconnect(), and use async for with stream().
Resume terminal output after reconnect
from hyperbrowser import Hyperbrowser
client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create({"image_name": "node"})
terminal = sandbox.terminal.create({"command": "bash"})
connection = terminal.attach(cursor=10)
for event in connection.events():
print(event)
Cache remote Dockerfile builds
Use the public context fingerprint when deriving a cache name. It uses the same
Dockerfile source selection and .dockerignore rules as remote packaging,
including file contents, modes, paths, and symlinks. It ignores timestamps and
does not compress or stage the context on disk.
from hyperbrowser.build_context import docker_build_context_fingerprint
fingerprint = docker_build_context_fingerprint("./app")
# Include build options such as platform and image_init in your cache key too.
image_name = f"app-{fingerprint[:32]}"
build = client.sandboxes.build_image_from_dockerfile(
context_path="./app",
image_name=image_name,
expected_context_fingerprint=fingerprint,
)
If the archived inputs differ from the fingerprint, the SDK raises
DockerBuildContextChangedError before creating or uploading a build. Compute a
fresh fingerprint and repeat the lookup/build operation. Use the same dockerfile
and force_full_context selection when fingerprinting and building (the latter
is named remote_full_context on the build method). The expected fingerprint is
supported only for remote builds.
Fingerprinting streams included files and performs blocking I/O. Async callers
should use await asyncio.to_thread(docker_build_context_fingerprint, "./app")
(Python 3.9+) or an executor. It does not resolve mutable base-image tags or
network resources fetched by a Dockerfile; rebuild explicitly when those change.
Image listings distinguish ready from uploaded: a completed team image can
be ready to launch before its durability backup is uploaded. ready is None
when talking to an older server. Keep the returned image ID to pin that revision.
Reuse an image or join a build
get_or_build_image provides the same operation on sync and async clients. Give
it either a remote Dockerfile context or a local Docker image. It derives a name
from the input identity and image initialization options, reuses a ready team
image, or submits a build and joins a compatible concurrent build automatically.
The optional prefix is a namespace, not a fixed image alias: different inputs
produce different names under the same prefix.
resolved = client.sandboxes.get_or_build_image(
context_path="./app", # alternatively: docker_image="local/app:latest"
image_name_prefix="my-app",
wait_timeout=3600,
)
print(resolved.outcome) # "reused", "joined", or "created"
sandbox = client.sandboxes.create({
"image_name": resolved.image_name,
"image_id": resolved.image_id,
})
With wait=False, a submitted/joined build is returned as resolved.build;
image_id is populated only when ready. find_ready_image(name) exposes the
exact-name lookup separately. Older servers fall back to uploaded-image reuse.
The public hyperbrowser.image_builds.image_build_name helper lets integrations
derive the same name from an existing context fingerprint or Docker image digest.
Passing expected_context_fingerprint or expected_image_digest avoids repeating
identity discovery; supply a fresh identity for each resolution request. Changes
between identity discovery and packaging are rejected instead of published under
the wrong name. Local Docker images must already be available in the daemon.
Automatic local-image identity discovery requires a Docker CLI and Engine
supporting API 1.49 or newer (Docker 28.1+) for platform-specific inspection.
Upgrade Docker and check for an older DOCKER_API_VERSION override if the helper
reports this requirement. Remote Dockerfile builds do not require local Docker.
The existing explicit-name import method retains its inspection fallback.
force_build=True skips ready-image lookup but still joins matching active builds
and permits existing layer/artifact caches. Use it to refresh mutable base tags or
external Dockerfile downloads. Joining does not change an existing builder's
resources. Lookup and creation use separate API calls; if another build completes
between them, an additional revision can be submitted.
Each caller owns its polling timeout. Canceling that wait does not cancel an
accepted backend build. Uploads have a separate inactivity allowance
(upload_timeout=600 by default), not a total upload-duration limit. The existing
build_image_from_dockerfile and build_image_from_docker_image methods retain
their explicit-name behavior and continue to report build conflicts directly.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Metadata
Release files for hyperbrowser 1.13.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 | |
|---|---|---|---|
| hyperbrowser-1.13.0.tar.gz | 187.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hyperbrowser-1.13.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 433.8 kB
Release files / hyperbrowser-1.13.0.tar.gz
| Download URL | hyperbrowser-1.13.0.tar.gz |
|---|---|
| Size | 187.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f10e8791856da2be1d962e5157174e3d800be9c034eae66ff983d0a050dfe700
|
|
BLAKE2b-256 checksum How to use checksums |
b74bc36e844bce4448cf9fb3c2200667af7eae808b3155677e9acbe56ad1282a
|
| 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 15, 2026.
Transparency logRelease files / hyperbrowser-1.13.0-py3-none-any.whl
| Download URL | hyperbrowser-1.13.0-py3-none-any.whl |
|---|---|
| Size | 246.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
068dde6a405163eb3759b0b619fa53f49bf927beeb228826b25c4f4837a9bbcb
|
|
BLAKE2b-256 checksum How to use checksums |
22b07f29bda2011e3bd6996c7e1a18385f72e9faeade6ee7fe526e43fbee2201
|
| 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 15, 2026.
Transparency log