VeilBrowserDocs

Authentication

The VeilBrowser credentials, where each one belongs, and how to manage organization API keys.

Mixing up the credentials is the most common integration mistake, so start here before writing any client code.

The credentials

CredentialSent asIssued byOpens
Dashboard sessionCookie or Authorization: BearerSigning in at veilbrowser.netProfile CRUD, session history, organizations, billing
Organization keyX-API-KeyDashboard → Settings → API keysWorker-to-cloud calls, and /launch on a worker
Worker keyX-API-KeyYou, as the worker's API_KEYA worker's REST surface, its worker-wide listings, and any single browser
Control tokenX-Veil-Token, a veil_ctl_<launchId> cookie, or ?ct= onceThe worker, at launchEvery /browsers/<id>/* route, for that one launch
View ticketA veil_view_<launchId> cookie, or ?t= onceThe workerThat launch's VNC stream and its assets, and nothing else

The first two go to api.veilbrowser.net. The rest go to a worker on your own network. They are never interchangeable.

Dashboard session

Profile management is a dashboard operation. GET/POST/PATCH/DELETE /v1/profiles, GET /v1/sessions, the organization endpoints and everything under /v1/billing require a signed-in session, established by Better Auth at sign-in.

Scripts that manage profiles carry the same session cookie a browser would:

terminal
curl https://api.veilbrowser.net/v1/profiles \
  -H "Cookie: better-auth.session_token=..."

Organization keys are not a substitute here

An organization API key will not authenticate profile CRUD. It is scoped to the worker integration endpoints listed below. If you need unattended profile provisioning, drive it from a service account session.

Organization key

This is the key every self-hosted deployment depends on. It identifies the organization, authorizes the private image pull, and gates each browser launch at runtime.

Where it is used

PurposeHow it travels
Image pullLowercased, as a path segment: registry.veilbrowser.net/<org-key-lower>/local-api
Worker runtimeThe VEIL_KEY environment variable on each worker
LaunchAn X-API-Key or Authorization: Bearer header on the /launch upgrade, or inside ?env=

Cloud endpoints that accept it:

  • POST /v1/license/authorize — concurrency check before a launch
  • GET /v1/profiles/:id/config — fetch a profile's launch configuration
  • POST /v1/profiles/:id/sessions and DELETE /v1/profiles/:id/sessions/:sessionId — session lifecycle
  • GET /v1/chromium/releases and GET /v1/chromium/releases/:version/download — browser build catalog

Creating one

Sign in, open Settings → API keys, and name the key for where it will run — prod-cluster-eu, say. Keys are scoped to the organization that is active when you create them, so confirm the right one is selected first. The full key is shown once: VeilBrowser stores only a SHA-256 hash, so a lost key can be revoked and replaced but never recovered. The Connect page shows it again alongside its lowercased form, which is the registry path segment.

Rotating

The key is both a pull credential and a runtime credential, so rotate in this order to avoid an outage.

Create the replacement

Add a second key before disabling anything. Both are valid at once.

Roll the fleet

Update VEIL_KEY in each worker's environment and restart. Workers fail closed, so a worker still holding a revoked key stops launching rather than launching unlicensed.

Verify, then revoke

Confirm every worker is healthy and launching, then delete the old key from Settings → API keys.

Treat it like a password

An organization key grants image pulls and browser launches against your plan. Store it in your secret manager, never in a repository, and rotate it immediately if it leaks.

Worker key

A worker's own shared secret, set as API_KEY and sent as X-API-Key. It has nothing to do with your VeilBrowser account — it exists so that only your automation can reach the worker. It opens the REST surface (/profiles, /sessions, /fleet), the worker-wide listings, and any single browser, which is how the operator console reaches them.

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>" }'

API_KEY falls back to VEIL_KEY, so a worker configured with only an organization key uses it for both. A worker with neither generates a token at boot and prints it once — which changes on every restart, so it is a way to try a worker out rather than a way to run one. There is no configuration in which a worker is unauthenticated.

Per-launch tokens

A control token is minted when a browser is created and is scoped to that one launchId. The launch socket is the one place a client has already proved it holds the organization key, so it is where the token is handed over: Veil.getSessionInfo returns it as controlToken, the REST launch response embeds it in every URL, and Veil.getBrowserURLs comes back with ?ct= already on each one. Reconnecting over webSocketDebuggerUrl therefore works unchanged, because a CDP client that cannot set headers on an upgrade carries the token in the query string.

Both cookie names carry the launchId with every non-alphanumeric character stripped, so two sessions open in one browser cannot overwrite each other.

The pages a human opens (/view and /browsers/<id>/) accept ?ct= exactly once: they convert it into a path-scoped HttpOnly cookie, send Referrer-Policy: no-referrer and redirect to the clean URL, so the token leaves the address bar, the browser history and any Referer a page inside the session would send. Control tokens last 12 hours and view tickets 15 minutes, both signed with a per-boot secret, so a worker restart invalidates every outstanding one.

Two things follow that are worth knowing before you debug a 404:

  • A missing or wrong token on a per-browser route answers 404 unknown browser, exactly as an id that never existed does. A distinct 403 would confirm that a guessed launchId names a live session. The worker-wide listings answer 401 instead, since they leak nothing about a specific launch, and the VNC stream answers 403 forbidden because it is gated on the view ticket rather than on the same lookup.
  • View and control are separate signed scopes. A view ticket streams the desktop and nothing else: it cannot navigate, close, screenshot or attach CDP. A control token is the stronger of the two and also opens the stream, which is how the viewer frames VNC off the cookie it already holds.

Operators never handle a worker key in a browser. Cluster-manager calls POST /browsers/<id>/view-ticket server-side and hands the console the short-lived pair it gets back.

Per-launch tokens gate sessions, not the worker

/launch itself is reachable by anyone who can dial the port. Keep workers behind the cluster-manager edge rather than exposing one directly. See Edge security & TLS.

Next steps

Was this page helpful?

On this page