JavaScript SDK
@stealth-browser/cloud-sdk: a dependency-free client for the platform API, for Node ≥ 20 and browsers.
Install
The package is a workspace package in the repository until the platform launch; publishing to npm follows it. Until then, install it from a checkout:
npm install /path/to/stealth-browser/packages/cloud-sdkNo runtime dependency beyond the global fetch. It is not the local [stealth-cli SDK](/sdks/cli/), which launches Camoufox in-process; the two share the observation and action vocabulary, not code.
Client
import { StealthBrowser, StealthApiError, verifyWebhook } from '@stealth-browser/cloud-sdk'; const client = new StealthBrowser({ apiKey: process.env.STEALTH_API_KEY, baseUrl: process.env.STEALTH_API_URL });new StealthBrowser({ apiKey, baseUrl?, fetch?, timeoutMs?, retries? }). The key's project is the project. timeoutMs (60 s) bounds each request; event streams are bounded by an idle watchdog instead. retries (2) applies to idempotent requests only — GETs, and creates, which carry an Idempotency-Key (a UUID unless you pass one).
Surfaces
| Surface | Methods |
|---|---|
client.run(task, options) | Create and wait; options are run fields (model_id, session_id, browser_id, workspace_id, profile_id, judge, credential_ids, allowed_domains, …) plus pollMs, timeoutMs, signal, idempotencyKey, onEvent |
client.runs | create, get, list, cancel, wait(id), events(id) (async iterator), message(id, text), withdrawMessage(id, messageId) |
client.agentSessions | list, get, rename(id, title), delete(id) |
client.models | list |
client.browsers | create → CloudBrowser, attach(id), get, list, stop |
client.assets | list(browserId), get, text, content (bytes), retry |
client.profiles | list, create, get, update, import, save(id, { browser_id }) |
client.workspaces | list, create, get, update, sessions; files.list, files.upload(id, path, bytes), files.download, files.text, files.move, files.folder, files.delete, files.archive(id, { ids }) |
client.schedules | list, get, history, preview, runNow |
client.integrations, client.proxies | list, get; policy, credentials |
client.billing | get, setBudget(usd | null), statement({ month }), ledger({ limit }), rates |
client.history, client.usage, client.project | list(query), get({ days }), current |
client.request(method, path, { body, bytes, query, responseType }) | Any other route, with the same errors and retries |
verifyWebhook({ body, headers, secret }) | Standard Webhooks v1 check with Web Crypto; returns the event or throws webhook_signature |
A revision a write needs (update, move, folder, delete, archive, import, save, retry) is fetched from the resource when you do not pass one. What the API reserves for console sessions — credentials, webhook endpoints, schedule and integration management, deletions — is not here; the API answers permission_denied for a key.
CloudBrowser
const browser = await client.browsers.create({ settings: { locale: 'en-US' } });await browser.ready(); // waits for `running`await browser.navigate('https://example.com'); // { url, title }const state = await browser.observe({ boxes: true }); // the observation object, snake_case as the API sends itconst link = state.targets.find((t) => t.role === 'link');await browser.click(link.ref);const { text } = await browser.text({ format: 'markdown' }); // read-only: keeps the frameconst { matches } = await browser.find({ role: 'button', name: 'sign in' });await browser.stop();act(name, params) runs any action with the registry's parameter names; the convenience methods are click(ref), type(ref, text, { slowly, submit }), select(ref, values), press(key, ref?), scroll(delta_y, ref?), hover(ref), wait({ ms, state }), clickAt(x, y), back(), forward(), reload(), switchTab(tab), closeTab(tab?), newTab(url?), dialogPolicy(policy, prompt_text?), and the read-only find(query) and text({ format, max_chars }). preview() is the observation plus a JPEG image; snapshot() is the tree alone; events() iterates the browser's events to its terminal state; assets() lists its downloads and recordings; stop() resolves with the final resource even when the API reports cleanup_failed.
Frames: the client holds the last observation's frame_id, observes when it has none, re-observes once on stale_frame, drops the frame after a page-changing action and keeps it after a read-only one.
Events
runs.events(id) and browser.events() open GET …/events?stream=1 and read server-sent events until the end event, reconnecting from the last sequence up to three times when the connection drops or goes quiet for 45 s; when the API answers JSON instead, or refuses the stream with rate_limited (its bound on open streams per key and project), they poll the cursor every pollMs. client.diagnostics counts streams opened and polls made. run(task, { onEvent }) streams on the side while waiting; a timeout or abort stops the stream too.
Errors
Every failure is a StealthApiError: status, code (the API's — see Errors), request_id, details and local. A request the contracts reject is thrown with local: true before anything is sent. Without a response the code is network_error, bad_response, sdk_timeout or aborted.
try { await browser.click('e99');} catch (error) { if (error instanceof StealthApiError && error.code === 'stale_frame') { /* the client already retried once */ } throw error;}In a browser page
The contracts use globalThis.crypto and a portable IP check, so the package loads in a browser as well as in Node. The API must allowlist the page's origin for that to be useful, and a key in a page is visible to everyone who loads it — see Authentication.
