LelantosLelantos
Browser

Browser SDK

The first-party lelantos.browser.* namespace - create and connect browser sandboxes over CDP, manage contexts and artifacts, and drive the NDJSON RpcClient.

The lelantos.browser namespace is the first-party way to provision and drive browser sandboxes. It is exposed through the modern createLelantos() surface in @lelantos-ai/sdk.

import { createLelantos } from '@lelantos-ai/sdk';

const lel = createLelantos({
  apiKey: process.env.LELANTOS_API_KEY!,
  domain: 'lelantos.ai',
});

There are two ways to drive a browser sandbox:

  1. Over CDP via Playwright - createBrowser() / connect() return a connected Playwright Browser. Requires playwright installed.
  2. Over the NDJSON action API - rpc() returns an RpcClient for page.navigate / page.snapshot / page.evaluate. Needs no Playwright.

Playwright is an optional peer dependency. The CDP helpers lazy-import it; install it only if you use them (npm i playwright). The RpcClient path has no Playwright dependency.

Browser sandboxes are stateful (live browser state + fingerprints) and do not support durable pause/recover. The lifecycle is create → use → close. To reattach to a still-running sandbox, use connect(). See Browser Concepts.

createBrowser(options?)BrowserHandle

Provisions a browser sandbox (POST /browser-sandboxes), polls until it is running with a wsEndpoint + accessToken, opens the bearer-subprotocol bridge, and connectOverCDPs a Playwright Browser.

const handle = await lel.browser.createBrowser({
  engine: 'chromium',          // 'chromium' (default) | 'firefox'
  timeout: 600,                // sandbox lifetime in seconds (default 600)
  templateID: 'my-uni-variant',// optional custom uni-browser template
  metadata: { run: 'demo' },   // optional
  proxy: {                     // optional per-session BYO proxy (forces a cold boot)
    kind: 'http', host: 'proxy.example.com', port: 8080,
  },
  pollTimeoutMs: 120000,       // max wait for `running` (default 120000)
  pollIntervalMs: 1000,        // poll cadence (default 1000)
});

const page = await handle.browser.newPage();
await page.goto('https://example.com');
await handle.close();

The SDK's default timeout is 600 seconds. The REST API's own default for POST /browser-sandboxes is 3600 seconds - they differ, so set timeout explicitly if you depend on a specific lifetime.

CreateBrowserOptions

FieldTypeDefaultNotes
engine'chromium' | 'firefox''chromium'Backing engine.
templateIDstring-Custom uni-browser template override.
timeoutnumber600Sandbox lifetime, seconds.
metadataRecord<string,string>-Arbitrary metadata.
proxyBrowserProxy-Per-session BYO proxy. Forces a cold boot (warm-pool browsers launch proxy-less).
pollTimeoutMsnumber120000Max wait for the sandbox to reach running.
pollIntervalMsnumber1000Poll cadence.

BrowserHandle

MemberTypeDescription
browserBrowserThe connected Playwright Browser.
sandboxIdstringThe sandbox ID.
contextIdstring | nullSet when a per-context proxy was requested at create time.
wsEndpointstringThe authenticated CDP endpoint (wss://{id}.{domain}/cdp).
accessTokenstringThe bearer token for the CDP / NDJSON upgrade.
contextsBoundContextsThis sandbox's contexts sub-namespace.
artifactsBoundArtifactsThis sandbox's artifacts sub-namespace.
rpc()Promise<RpcClient>Lazily opens and caches an RpcClient for this sandbox.
close()Promise<void>Closes the browser + bridge + any opened RPC client, then deletes the sandbox.

connect(sandboxId)BrowserHandle

Reattaches to an already-running browser sandbox (reconnect-to-running). The SDK fetches the live wsEndpoint + accessToken and connects a CDP client.

const handle = await lel.browser.connect('i-abc123');
const page = (await handle.browser.contexts())[0].pages()[0];

A handle from connect() only attached - its close() tears down the local browser/bridge but does not delete the sandbox. Kill it explicitly with lel.browser.destroy(sandboxId).

list()BrowserSandboxRecord[]

Lists the team's browser sandboxes.

get(sandboxId)BrowserSandboxRecord

Fetches one browser-sandbox record (status, wsEndpoint, ndjsonEndpoint, accessToken, …).

destroy(sandboxId)void

Deletes (tombstones) a browser sandbox.

rpc(sandboxId, opts?)RpcClient

Opens an RpcClient for an already-running sandbox without Playwright. Fetches the record, requires running with an ndjsonEndpoint + accessToken, then opens the bearer-subprotocol WebSocket.

const rpc = await lel.browser.rpc('i-abc123', { defaultTimeoutMs: 30000 });
await rpc.navigate('https://example.com');
await rpc.close();

Contexts

A context is an isolated browsing context (cookies, storage) on a running sandbox, optionally with its own BYO proxy. Available unbound on lel.browser.contexts.* (pass sandboxId per call) or bound on a handle as handle.contexts.*.

// On a handle
const ctx = await handle.contexts.create({
  proxy: { kind: 'http', host: 'proxy.example.com', port: 8080 },
});
const contexts = await handle.contexts.list();
await handle.contexts.close(ctx.contextId!);

// Unbound
await lel.browser.contexts.create('i-abc123');
await lel.browser.contexts.list('i-abc123');
await lel.browser.contexts.close('i-abc123', 'ctx-id');
MethodReturnsDescription
create(options?)BrowserContextCreate a context, optionally with a proxy.
list()BrowserContext[]List the sandbox's contexts.
close(contextId)voidClose a context.

Artifacts

Artifacts are screenshots, PDFs, traces, videos, or HAR files persisted server-side. Available unbound on lel.browser.artifacts.* or bound on a handle as handle.artifacts.*. The bound form also offers capture helpers that drive the connected Playwright browser, then upload.

// Capture + persist in one call (bound form)
const shot = await handle.artifacts.screenshot({ fullPage: true });
const pdf = await handle.artifacts.pdf();           // chromium only
const list = await handle.artifacts.list();

// Upload pre-captured base64 bytes directly
await lel.browser.artifacts.upload('i-abc123', 'screenshot', base64Png, {
  contentType: 'image/png',
});
MethodReturnsDescription
screenshot(opts?)BrowserArtifactCapture the active page via Playwright, then persist. { fullPage } optional.
pdf()BrowserArtifactCapture a PDF, then persist. Chromium only - Playwright page.pdf() is unavailable on Firefox.
upload(kind, dataBase64, opts?)BrowserArtifactPersist pre-captured base64 bytes.
list()BrowserArtifact[]List persisted artifacts (newest first).

The server stores client-captured bytes - it does not capture for you. Each BrowserArtifact carries a downloadURL pointing at the artifact download route; fetch it with your normal auth headers. See Browser Concepts → Artifacts.

ArtifactKind is 'screenshot' | 'pdf' | 'trace' | 'video' | 'har'.

RpcClient

A single-WebSocket NDJSON client to a sandbox's /ndjson endpoint. Construct it via lel.browser.rpc(sandboxId) / handle.rpc(), or directly with RpcClient.open(). See the RPC wire contract for the underlying frames.

const rpc = await handle.rpc();

const nav = await rpc.navigate('https://example.com'); // { page_id, url }
const snap = await rpc.snapshot();                      // { tree_yaml, title, element_count, page_id, url }
const ev = await rpc.evaluate('document.title');        // { value, page_id }

await rpc.close();

Methods

MethodReturnsDescription
RpcClient.open(opts)Promise<RpcClient>Dial the ndjsonEndpoint with the bearer subprotocol and resolve on the 101 upgrade.
send(action, params?, opts?)Promise<RpcResponse>Send one action and resolve with the matching-id frame. Use for actions without a wrapper.
navigate(url, opts?)Promise<Record<string,unknown>>page.navigate - returns { page_id, url }.
snapshot(opts?)Promise<Record<string,unknown>>page.snapshot - returns { tree_yaml, title, element_count, page_id, url }.
evaluate(expression, opts?)Promise<Record<string,unknown>>page.evaluate - returns { value, page_id }.
close()Promise<void>Close the socket and reject any in-flight requests.

The convenience wrappers return the action's data.* payload directly; send() returns the whole frame (read .data for the result).

On the wire the action verb field is action, but the SDK method takes the action name as its first argument (named method in the signature). send('page.navigate', { url }) emits { action: 'page.navigate', … }.

RpcSendOptions

FieldTypeDefaultNotes
sessionstring | nullthe client's sandboxIdThe session to bind to. Pass null to omit the field entirely.
timeoutMsnumberdefaultTimeoutMsPer-request timeout.

RpcClientOptions (for RpcClient.open)

FieldTypeDefaultNotes
ndjsonEndpointstring-The wss://{id}.{domain}/ndjson endpoint.
accessTokenstring-Opened as the bearer.<token> subprotocol.
sandboxIdstring-Default session for every request.
defaultTimeoutMsnumber30000Default per-request timeout.
openTimeoutMsnumber30000Max wait for the 101 upgrade.
onEvent(frame) => void-Sink for unsolicited / event frames (otherwise dropped).

RpcResponse and RpcError

A parsed response frame:

interface RpcResponse {
  id?: string;                      // echoed request id ('' on protocol/parse errors)
  success?: boolean;                // absent error frames are treated as failures
  data?: Record<string, unknown>;  // result payload on success
  error?: string;                   // message on success:false
  [key: string]: unknown;
}

A send() / wrapper call rejects with an RpcError on a success:false frame, a per-request timeout, or socket close. RpcError carries the method, requestId, and the raw response frame (if any).

See also

On this page