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.
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.
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>.
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:
const client = await page.createCDPSession();
const { vncUrl, devtoolsFrontendUrl } = await client.send("Veil.getBrowserURLs");| Method | Returns |
|---|---|
Veil.getBrowserURLs | launchId, workerId, webSocketDebuggerUrl, devtoolsFrontendUrl, viewUrl, jsonEndpointUrl, screenshotUrl, vncUrl |
Veil.getLaunchId | The launchId on its own |
Veil.getSessionInfo | launchId, 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.
| Endpoint | Auth | Purpose |
|---|---|---|
wss://<host>/launch/<referenceId>?reconnect=true | Organization key | Reattach to an existing browser, or launch if none holds that id |
wss://<host>/launch?firstPage=true | Organization key | Launch and attach to a page target rather than the browser, so a DevTools frontend has something to inspect |
wss://<host>/browsers/<id> | Control token | Reattach browser-level CDP to a running browser |
GET /browsers | Worker key | Live browsers on the worker (?simple=true for a compact form) |
GET /json | Worker key | Aggregated CDP page list across every live browser |
GET /browsers/<id>/urls | Control token | What Veil.getBrowserURLs returns, over HTTP |
GET /browsers/<id>/info | Control token | Session facts, uptime, connection count, resolved fingerprint, and a fresh token |
GET /browsers/<id>/json | Control token | The browser's CDP target list |
GET /browsers/<id>/view | Control token | The viewer. ?tab=vnc opens on the VNC pane |
GET /browsers/<id>/frontend | Control token | 302 to ./view — the URL devtoolsFrontendUrl returns |
GET /browsers/<id>/frontend/page/<targetId> | Control token | 302 straight to one page's inspector, bypassing the viewer |
GET /browsers/<id>/vnc/ | View ticket or control token | The live VNC stream. Trailing slash required |
GET /browsers/<id>/screenshot | Control token | A JPEG still of the current page. ?quality= is clamped to 1-100 |
POST /browsers/<id>/navigate | Control token | Send the first page target to {"url": "https://example.com"}. http/https only, 2048 characters at most |
POST /browsers/<id>/close | Control token | Close the browser and release its display |
POST /browsers/<id>/view-ticket | Worker key | Mint a view ticket and control token for an operator, server-side |
GET /cluster/health | None | Registry 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.