VeilBrowserDocs

Connect

Launch a browser on one of your workers and drive it with Puppeteer, Playwright or any CDP client.

Browsers run on your machines. A worker (the Local API) is the thing that launches them; your automation connects to it over the Chrome DevTools Protocol.

One worker serves one surface, so there is nothing to turn on. A fleet is the same worker replicated behind the cluster-manager load balancer, and you connect to the balancer instead of to a box.

Two ways to start a browser

Both authorize the launch against your plan, apply the profile's browser build and proxy, and hand back the same CDP endpoint.

Connect and the browser is launched for you — no REST call, no second step. Stock Puppeteer and Playwright work unmodified.

launch.ts
import puppeteer from "puppeteer-core";

const env = {
  VEIL_KEY: "<organization-api-key>",
  // Launch an existing cloud profile…
  profileId: "<profile-uuid>",
  // …or create one from criteria:
  // spec: { os: "win", browserVersion: 147 },
};

const browser = await puppeteer.connect({
  browserWSEndpoint:
    "wss://connect.example.com/launch?env=" +
    encodeURIComponent(JSON.stringify(env)),
});
const page = await browser.newPage();
await page.goto("https://example.com");

Playwright is the same URL through chromium.connectOverCDP(url).

The envelope also goes in ?envc= lz-string-compressed, which is what a launcher UI needs when a full profile spec will not fit in a URL. Pass the organization key as an X-API-Key or Authorization: Bearer header where your client can set headers — a key in the query string lands in access logs, shell history and any Referer a page in the session sends.

Ask a worker to launch, then attach. Useful when the thing that decides to launch is not the thing that drives the browser.

terminal
curl -X POST https://worker.example.com/sessions/profile \
  -H "X-API-Key: $WORKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "profileId": "<profile-uuid>" }'

Supply either profileId for an existing profile or spec to create one from criteria, never both. Optional browser and proxyOverride objects adjust that single launch.

The 201 carries id, launchId, status, startedAt, webSocketDebuggerUrl, devtoolsFrontendUrl and viewUrl. Stop it with DELETE /sessions/<id>.

attach.ts
const { data } = await res.json();
const browser = await puppeteer.connect({
  browserWSEndpoint: data.webSocketDebuggerUrl,
});

The CDP URL

Whichever door you came in, webSocketDebuggerUrl is addressed to the worker:

wss://<host>/browsers/<launchId>/devtools/browser/<guid>?ct=<capability>

It resolves from anywhere that can reach the worker, and the ct query parameter is the per-launch capability the /browsers/* routes require — treat it as a secret. Behind the load balancer the path is prefixed with /w/<workerId> so reconnects reach the box holding that browser. A standalone worker that cannot infer its own public address needs VEIL_SERVER_ORIGIN.

Selenium needs a host and port

debuggerAddress takes 127.0.0.1:<port>, not a URL, so Selenium has to run beside the worker and read debugPort from the launch response rather than parsing webSocketDebuggerUrl.

Ask the session about itself

The URLs for watching and inspecting a session come back from a single CDP call, so a client never has to construct them:

urls.ts
const client = await page.createCDPSession();
const { vncUrl, devtoolsFrontendUrl } = await client.send("Veil.getBrowserURLs");
MethodReturns
Veil.getBrowserURLslaunchId, workerId, webSocketDebuggerUrl, devtoolsFrontendUrl, viewUrl, jsonEndpointUrl, screenshotUrl, vncUrl
Veil.getLaunchIdThe launchId on its own
Veil.getSessionInfolaunchId, workerId, sessionId, profileId, referenceId, display, launchedAt, plus controlToken, controlTokenParam and controlExpiresIn

The worker answers these itself and passes everything else through to Chromium, so they work from a browser-level or a page-level session. Every URL comes back with this launch's capability already embedded, so either viewer URL is ready to paste into a browser tab.

Reconnect and address one browser

Give a launch a stable referenceId and you can come back to it. The Auth column names the credential each route needs; see Authentication.

EndpointAuthPurpose
wss://<host>/launch/<referenceId>?reconnect=trueOrganization keyReattach to an existing browser, or launch if none holds that id
wss://<host>/launch?firstPage=trueOrganization keyLaunch and attach to a page target rather than the browser, so a DevTools frontend has something to inspect
wss://<host>/browsers/<id>Control tokenReattach browser-level CDP to a running browser
GET /browsersWorker keyLive browsers on the worker (?simple=true for a compact form)
GET /jsonWorker keyAggregated CDP page list across every live browser
GET /browsers/<id>/urlsControl tokenWhat Veil.getBrowserURLs returns, over HTTP
GET /browsers/<id>/infoControl tokenSession facts, uptime, connection count, resolved fingerprint, and a fresh token
GET /browsers/<id>/jsonControl tokenThe browser's CDP target list
GET /browsers/<id>/viewControl tokenThe viewer. ?tab=vnc opens on the VNC pane
GET /browsers/<id>/frontendControl token302 to ./view — the URL devtoolsFrontendUrl returns
GET /browsers/<id>/frontend/page/<targetId>Control token302 straight to one page's inspector, bypassing the viewer
GET /browsers/<id>/vnc/View ticket or control tokenThe live VNC stream. Trailing slash required
GET /browsers/<id>/screenshotControl tokenA JPEG still of the current page. ?quality= is clamped to 1-100
POST /browsers/<id>/navigateControl tokenSend the first page target to {"url": "https://example.com"}. http/https only, 2048 characters at most
POST /browsers/<id>/closeControl tokenClose the browser and release its display
POST /browsers/<id>/view-ticketWorker keyMint a view ticket and control token for an operator, server-side
GET /cluster/healthNoneRegistry health for the worker

Watching a session

/browsers/<id>/view frames the two raw surfaces and puts the session's facts beside them: launch id, worker, profile, operating system, display and idle timeout. It opens on DevTools and switches to VNC when the browser has a pooled display; a headless browser gets DevTools only.

DevTools is served by the browser binary itself and proxied through the worker, so it needs no internet access and always matches the running Chromium. It carries a screencast panel, so it doubles as a way to watch and drive the page. VNC is the wider view: the whole desktop, including native dialogs, extension popups and anything outside the page viewport.

Each headful browser gets its own KasmVNC X server from a pool sized by VEIL_DISPLAY_COUNT, which is therefore also the cap on concurrent headful sessions per worker. Set VEIL_VNC_MODE to viewonly to make streams non-interactive, or off to disable them. Those servers bind to loopback behind HTTP basic auth, and the worker generates that password for itself at boot: the proxy is the only holder, and it is never forwarded to a viewer.

Operators get the same two viewers, fleet-wide and behind console login, on the cluster manager's Live sessions page.

Orchestration

Provision profiles through the cloud API or the dashboard; launch and stop them on workers. Design automation so a session can be retried safely: launch, do one unit of work, stop, move on.

Every launch calls the cloud before spawning. If the cloud is unreachable or the organization is at its concurrency limit the launch is refused — running sessions are never killed. Budget for one cloud round-trip per launch. See Licensing & concurrency.

Next steps

  • Authentication — which credential opens what.
  • API reference — full request and response shapes.
  • CLI — drive profiles and sessions from the terminal.
  • SDK — typed clients for both surfaces.
  • Errors — the response envelope and error codes.
Was this page helpful?

On this page