Official Python client for the Supernatural stealth browser-as-a-service API.
pip install sprntrl
# Optional: Playwright integration
pip install 'sprntrl[playwright]' && playwright install chromiumfrom sprntrl import Sprntrl
client = Sprntrl() # reads SPRNTRL_API_KEY from env
session = client.sessions.create(os="macos", location="America/New_York")
# browser_session is a context manager that waits for the session,
# connects Playwright, and closes the browser + Playwright on exit.
# auto_whitelist=True registers your IP (CDP access is IP-gated).
with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
page = browser.contexts[0].new_page()
page.goto("https://bot.sannysoft.com")
page.screenshot(path="out.png")
client.sessions.stop(session["id"])import asyncio
from sprntrl import AsyncSprntrl
async def main():
async with AsyncSprntrl() as client:
session = await client.sessions.create(os="macos", location="America/New_York")
async with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
page = await browser.contexts[0].new_page()
await page.goto("https://example.com")
await client.sessions.stop(session["id"])
asyncio.run(main())If you want to manage the browser lifecycle yourself:
browser = client.sessions.connect(session_id, auto_whitelist=True)
# ... your code ...
browser.close()Or to hand the raw WebSocket URL to any CDP client (chrome-remote-interface-python, raw websockets, etc.):
url = client.sessions.cdp_url(session_id)
# url = "wss://api.supernatural.sh/api/v1/sessions/<id>/cdp"sessions.create(os, location, ...) takes os ("macos" | "windows") and location (IANA timezone), plus:
persistent+session_name— keep the browser profile across stop/resume (see below).captcha_solver— auto-solves hCaptcha, Turnstile, reCAPTCHA and more; billed per solve.isolated_world— defaultTrue: automation runs in a V8 world hidden from the page. Keep it on for stealth — passFalseonly if you must access page JS globals (main-world execution is detectable).headless— defaultTruefor API/SDK callers. PassFalseto get the live browser view in the dashboard.block_images— defaultFalse. Disables image loading session-wide; cuts bandwidth and speeds up loads.label— pins the proxy-pool match to a specific pool row atlocation(one of the labels fromlist_locations(), e.g."Kentucky, US"). Ignored for BYO-proxy sessions.proxy— bring your own proxy as a URL string ("socks5://user:pass@host:1080") or dict ({"protocol": ..., "host": ..., "port": ..., "username": ..., "password": ...}). HTTP/HTTPS/SOCKS5.extensions— inline Chrome extensions for ephemeral sessions (see below).
locs = client.sessions.list_locations()
# {"options": [{"label": "Kentucky, US", "location": "America/New_York"}, ...],
# "accepts_iana": True, "iana_examples": ["America/New_York", ...]}Pool users must pick location (and optionally label) from options. accepts_iana: True means BYO-proxy users may pass any IANA timezone as location instead.
Create with persistent=True to keep the browser profile (cookies, storage, fingerprint) across runs:
session = client.sessions.create(
os="macos", location="America/New_York",
persistent=True, session_name="my-profile",
)
# ... use it ...
client.sessions.stop(session["id"])
# Later: relaunch with the same identity. All overrides optional —
# omitted values keep what's stored on the profile.
session = client.sessions.resume(session["id"], headless=False)
# Done with the profile entirely:
client.sessions.delete_persistent(session["id"])resume() accepts os, location, label, captcha_solver, isolated_world, headless, block_images, and proxy overrides. Changing os or location rebuilds the profile's pinned fingerprint — an intentional one-time identity drift; changing location on a pool session also re-assigns the pool proxy for the new region. Supplying proxy switches a pool session to BYO — switching BYO back to pool is not supported (delete + recreate).
Move files in and out of a running session:
client.sessions.files.upload(sid, "input.csv", open("input.csv", "rb"))
files = client.sessions.files.list(sid)
data = client.sessions.files.download(sid, "report.pdf")Uploads are capped at 100 MB per request.
Ephemeral sessions take inline extensions at create — each spec sets exactly one of webstore_url, crx_url, or upload_b64:
session = client.sessions.create(
os="macos", location="America/New_York",
extensions=[{"webstore_url": "https://chromewebstore.google.com/detail/..."}],
)Persistent profiles manage extensions via the sub-resource instead, so the set survives stop/resume:
ext = client.sessions.extensions.add(sid, webstore_url="...") # or upload= / crx_url=
client.sessions.extensions.list(sid)
client.sessions.extensions.set_enabled(sid, ext["id"], False)
client.sessions.extensions.remove(sid, ext["id"])Manifest V3 only (Chromium 148 dropped MV2); max 16 per profile; uploads capped at 50 MiB. Changes take effect at the next session start — stop + resume to apply.
| Env var | Default |
|---|---|
SPRNTRL_API_KEY |
— |
SPRNTRL_BASE_URL |
https://api.supernatural.sh |
Or override per client:
client = Sprntrl(api_key="sk_...", base_url="https://api.supernatural.sh", timeout=30, max_retries=2)client.sessions— create, list, list_active, list_history, list_resumable, list_persistent, list_locations, get, stop, resume, delete_persistent, wait_until_ready, connect, browser_session, cdp_urlclient.sessions.files— list, download, uploadclient.sessions.extensions— add, list, set_enabled, remove (persistent profiles)client.profiles— create, list, get, update, duplicate, deleteclient.templates.list()client.ip_whitelist— list, add, removeclient.usage— current, historyclient.user— me, update, update_settings, change_passwordclient.api_keys— list, create (full key returned ONCE), revoke
from sprntrl import Sprntrl, APIError, RateLimitError, AuthenticationError
client = Sprntrl()
try:
client.sessions.create(os="macos", location="America/New_York")
except RateLimitError as e:
print("rate limited:", e.status, e.body)
except AuthenticationError:
print("bad API key")
except APIError as e:
print("api error:", e.status, e)Transient errors (5xx, 429, 408, connection errors) are retried automatically up to max_retries times with exponential backoff.
- CDP access is IP-whitelist gated. The WebSocket at
/api/v1/sessions/:id/cdpdoes not accept bearer auth — instead, your public IP (as Cloudflare sees it) must be in your account's whitelist. Useclient.ip_whitelist.add("current")or passauto_whitelist=Truetosessions.connect. - Sessions start async.
sessions.createreturns immediately withstatus: "creating". Callsessions.wait_until_ready(id)before connecting, or just usesessions.connect()which waits for you. - API key is shown only once.
api_keys.create()returns the fullkeyfield exactly once — store it immediately.
MIT