WebX — User Guide
WebX is a verified browser-automation service: you give it a natural-language task, it drives a real Chrome browser with an LLM agent, validates its own work with an independent LLM judge, and streams every step to you over WebSocket. It also offers a browser-as-a-service Sessions API.
This guide documents the live deployment at:
Base URL:
https://webx.agentslab.hostWebSocket:wss://webx.agentslab.host
Everything below — every response body, event payload, and screenshot — was captured from this deployment on 2026-06-12.
1. Architecture
The service runs on a microk8s cluster as a single webx-server pod (Rust/axum). With WEBX_ORCHESTRATOR=process, it launches one isolated Chrome process per session from the pinned browser bundled in the server image; there is no shared Chrome sidecar. TLS is terminated by the NGINX ingress with an auto-renewing Let's Encrypt certificate. Planning and validation use the configured OpenAI-compatible model endpoint.
2. Authentication
All /api/*, /ws, /live, and /connect routes require an API key. Only
GET /health and GET /ready are public (the Kubernetes liveness/readiness
probes).
Three ways to present the key, checked in this order:
| Method | Example |
|---|---|
x-api-key header (recommended) | curl -H "x-api-key: $WEBX_TOKEN" … |
Authorization: Bearer | curl -H "Authorization: Bearer $WEBX_TOKEN" … |
?token= query parameter | wss://…/ws?session_id=…&token=$WEBX_TOKEN |
The query form exists because browser WebSocket clients cannot set headers. Keys are compared in constant time; per-project keys can be minted via POST /api/keys when a durable store is configured.
Without a valid key you get 401:
$ curl -s -o /dev/null -w "%{http_code}" https://webx.agentslab.host/api/engines
401
The master token for this deployment is stored on the server at
/root/webx-api-token(not in git). Rotate it by regenerating thewebx-envSecret — see §9.
3. Quickstart
TOKEN=$(ssh -i ~/.ssh/poc_server_new root@65.108.78.80 \
'microk8s kubectl -n webx get secret webx-env -o jsonpath="{.data.WEBX_API_TOKEN}" | base64 -d')
# 1. Is it up?
curl https://webx.agentslab.host/health
# OK
# 2. What engines are available?
curl -s -H "x-api-key: $TOKEN" https://webx.agentslab.host/api/engines | jq
[
{
"name": "chromium",
"supports_screencast": true,
"supports_fetch_auth": true,
"supports_screenshot": true,
"supports_pdf": true,
"supports_proxy": true,
"supports_persistent_context": true,
"supports_extensions": true,
"supports_selenium": true,
"supports_real_browser_fingerprint": true,
"supports_long_sessions": true,
"stateless": false,
"perception_modes": ["webmcp", "axtree", "dom", "pixels"],
"cost_score": 100,
"cold_start_ms": 2500
}
]
# 3. Run an agent task
curl -s -X POST -H "x-api-key: $TOKEN" -H "Content-Type: application/json" \
-d '{"task": "Go to https://example.com and report the main heading text on the page."}' \
https://webx.agentslab.host/api/tasks
# {"session_id":"c2910342-9963-4575-b9a7-7368bea9c165","status":"starting"}
# 4. Poll until terminal
curl -s -H "x-api-key: $TOKEN" \
https://webx.agentslab.host/api/tasks/c2910342-9963-4575-b9a7-7368bea9c165 | jq
{
"session_id": "c2910342-9963-4575-b9a7-7368bea9c165",
"status": "completed",
"task": "Go to https://example.com and report the main heading text on the page.",
"created_at": "2026-06-12T21:06:19.013409031Z",
"completed_at": "2026-06-12T21:06:27.629785460Z"
}
This task completed in 8 seconds, 1 agent step: the server seed-navigates to the URL named in the task, the LLM reads the page and answers, and the validator (a second, independent LLM pass) confirms the answer before the run is marked successful.
4. Agent task lifecycle
4.1 Start a task
POST /api/tasks
{"task": "<natural language instruction>", "session_id": "<optional, reuse an id>"}
→ {"session_id": "…", "status": "starting"}
For asynchronous automation, attach a per-run callback and your own correlation id:
{
"task": "Go to https://example.com and report the main heading.",
"callback": "https://automation.example.com/webhooks/webx",
"workflow_id": "order-42/run-7"
}
The callback is sent only after the terminal status and evidence are persisted.
Its JSON includes event, session_id, task_id, workflow_id, status,
task, answer, and timing. WebX retries a failed delivery twice and records
every attempt as a webhook_delivery audit row. Callback URLs must use HTTPS,
cannot contain credentials or fragments, cannot resolve to private/reserved
networks, are DNS-pinned for delivery, and cannot redirect. When
WEBX_CALLBACK_SIGNING_SECRET is configured, WebX adds X-WebX-Token,
X-WebX-Timestamp, and X-WebX-Signature: sha256=<HMAC-SHA256 of the exact body>.
4.2 Watch it live over WebSocket
Connect to the unified socket and you receive every agent event in real time:
import asyncio, json, websockets
async def watch(sid, token):
url = f"wss://webx.agentslab.host/ws?session_id={sid}&token={token}"
async with websockets.connect(url) as ws:
while True:
e = json.loads(await ws.recv())
print(e["type"], e.get("message", e.get("url", "")))
if e["type"] == "agent.finish":
return e
Real event sequence captured from this deployment:
{"type": "connection.ready", "session_id": "0b7893e3-…", "timestamp": "2026-06-12T21:04:24.155845881+00:00"}
{"type": "agent.started", "session_id": "0b7893e3-…", "task": "Go to https://example.com and report the main heading text on the page."}
{"type": "agent.observe", "session_id": "0b7893e3-…", "url": "about:blank"}
{"type": "agent.action", "session_id": "0b7893e3-…", "action": {"type": "navigate", "url": "https://example.com"}}
{"type": "agent.result", "session_id": "0b7893e3-…", "success": true, "message": "Navigated to 'https://example.com'"}
{"type": "agent.finish", "session_id": "0b7893e3-…", "success": true, "message": "Real WebX agent execution completed - 1 steps, duration: 11s"}
Event types you will see:
| Event | Meaning |
|---|---|
connection.ready | Socket accepted; events for your session_id will follow |
agent.started | Task accepted, agent loop starting |
agent.observe | One perception pass (current URL attached) |
agent.action | An action the agent is executing (navigate, click, fill, …) |
agent.result | Outcome of an action or of the whole run (success flag) |
agent.finish | Terminal event with step count and duration |
4.3 Stop a task
POST /api/tasks/{session_id}/stop
Cancellation is cooperative: the agent stops at the next step boundary and the browser is torn down immediately.
5. Live view — watch the browser as the agent works
GET /live?sessionId=…&token=… upgrades to a WebSocket that streams the session's screen.
Protocol: one JSON text preamble, then binary JPEG frames (one message per frame):
{"format": "jpeg", "sessionId": "c2910342-…", "type": "live"}
async with websockets.connect(f"wss://webx.agentslab.host/live?sessionId={sid}&token={token}") as ws:
preamble = json.loads(await ws.recv()) # text frame
while True:
frame = await ws.recv() # bytes = one JPEG image
if isinstance(frame, bytes):
display(frame)
A frame captured from a real run, while the agent was reading example.com:

Frames are sent on page damage, so a static page produces few frames — render the latest one you have.
5.5 Playground / admin portal
https://webx.agentslab.host/admin — a built-in playground + operator console (served by the server itself; the page is a static shell and every data call requires either your API key or a tenant-scoped browser identity session established through OIDC/SAML).

Light and dark themes (toggle in the header, follows your OS preference by default, ?theme=dark|light to force):

What it does:
- Playground composer: type a task (or pick an example chip), hit Run, and watch the browser pane and the live agent-event stream side by side
- Sessions table (auto-refreshing, backed by the durable store so history survives restarts): status, task, timings; filter by status
- Watch any running session — embeds the
/liveJPEG screencast - Stop a running session (cooperative cancel, immediate browser teardown)
- Playback / logs tab — the per-session recorder's network + console timeline for any session, including finished ones
- Receipts tab — the session's hash-chained authorization ledger
- Attribution tab — typed failure cause for failed runs
- Pending confirmations — approve/deny membrane-gated actions awaiting an operator
- Deep-link with
?session=<id>to open a session's detail directly; header links to the Grafana dashboard

5.7 One-shot Fetch API
POST /api/fetch renders a URL in a real, JS-capable browser and returns it in the formats you ask for — no session to manage. This is the stateless "give me this page" primitive (like Hyperbrowser Fetch / Steel /scrape).
curl -s -X POST -H "x-api-key: $TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://example.com","formats":["text","title","links","screenshot"]}' \
https://webx.agentslab.host/api/fetch
{
"url": "https://example.com/",
"title": "Example Domain",
"text": "Example Domain This domain is for use in documentation examples…",
"links": [{"url": "https://www.iana.org/domains/example", "text": "More information..."}],
"screenshot_base64": "iVBORw0KGgo…"
}
| Field | Meaning |
|---|---|
url (required) | Absolute http(s) URL to render |
formats | Any of text, html, links, title, screenshot. Omitted → text+links+title |
wait_ms | Extra settle time after load for JS-heavy pages (capped at 15s) |
A fetch provisions a short-lived isolated browser, drives it, and tears it down within the request, so it shares the global concurrency bound (429 when at capacity).
5.8 Multi-tenant projects, API keys, and isolation
POST /api/keys{ "project_id": "acme" }→ issues a scoped key (wx_…, shown once). Gated behind the master token.POST /api/keys/revoke{ "api_key": "wx_…" }(or{ "key_hash": "…" }) → revokes a key immediately. Rotation = revoke + issue.- Isolation is enforced: a project key only sees sessions created under its own project.
GET /api/sessions,/api/sessions/:id,/logs,/receipts,/attribution,/spend, stop, and release are all scoped — a cross-tenant id returns404(indistinguishable from "absent"). The master token is admin and sees every project.
5.9 Enterprise identity lifecycle
- OIDC:
GET /api/identity/oidc/:project/startstarts an auth-code flow with state, nonce, PKCE, and redirect allowlisting.GET /api/identity/oidc/:project/callbackvalidates issuer/audience/signature before creating a short-livedHttpOnly; Secure; SameSite=Laxbrowser session. - OIDC secrets: tenant OIDC
client_secretvalues are envelope-encrypted at rest withWEBX_EVIDENCE_ENCRYPTION_KEYunder anidentity:<project>scope. The config APIs and admin UI only return a stable redacted marker plus a configured flag; resubmitting that marker (or leaving the field blank) preserves the stored secret. - SAML:
GET /api/identity/saml/:project/metadataserves SP metadata,GET /api/identity/saml/:project/loginstarts the signed AuthnRequest flow, andPOST /api/identity/saml/:project/acsvalidates the signed response/assertion, destination/audience/InResponseTo, and time windows before creating the same browser session type. - SCIM:
POST /api/identity/projects/:project/scim/tokensmints a tenant-scoped provisioning bearer token (stored hashed). Use it against/scim/v2/:project/Usersand/scim/v2/:project/Groupsfor idempotent provisioning, PATCH-based lifecycle updates, and deprovisioning that revokes browser sessions (and any identity-bound API keys). - Console support: the Identity tab in
/adminedits tenant OIDC/SAML/claims settings, shows live user/group/session/token counts, and exposes recent identity audit events plus one-shot SCIM token creation.
5.9 Observability, replay, and human-in-the-loop
| Endpoint | What it gives you |
|---|---|
GET /api/sessions/:id/trajectory | The agent's step timeline — the persisted observe/act/result events, in order (the "what did the agent do" trace) |
GET /api/sessions/:id/replay | Captured screencast frames (base64 JPEG, oldest-first) for post-run replay |
GET /replay/:id | A standalone HTML replay player (scrub, play, speed) — open https://webx.agentslab.host/replay/<session>?token=<key> |
GET /api/sessions/:id/logs | Per-session network + console capture (filter with ?kind=) |
Replay is captured from the live screencast, so any session you watch (the playground opens /live automatically when a task runs) is recorded and replayable afterward. Frames are retained past the run; memory is bounded by keeping the most recent 64 sessions.
Human takeover — /live is bidirectional. Send a JSON text frame on the same socket to drive the page:
{"type": "mouse", "action": "click", "x": 420, "y": 230}
{"type": "scroll", "x": 200, "y": 200, "deltaY": 300}
{"type": "key", "text": "hello"}
{"type": "key", "key": "Enter"}
Mouse coordinates are page pixels. In the playground, click Take over on the Browser pane and your clicks/keystrokes are sent through — for logins, 2FA, or captchas the agent can't handle.
5.10 Usage metering and quotas
| Endpoint | What it does |
|---|---|
GET /api/billing/usage | Per-project browser-time totals (browser_seconds, browser_hours, session count). Master sees all projects; a project key sees only its own |
POST /api/billing/quota { "project_id": "acme", "max_browser_hours": 10 } | Set (or clear with null) a project's browser-hours cap (master only) |
Browser time is metered on every session's finalize. A project at/over its cap is rejected at session create with 429 quota_exceeded.
6. Sessions API — browser-as-a-service
For when you want the browser without the agent:
POST /api/sessions
{"timeout_ms": 120000, "keep_alive": false, "headless": true,
"viewport_width": 1280, "viewport_height": 800, "user_agent": null, "proxy_server": null}
Real response:
{
"session_id": "2f4484f1-21a2-47e4-80fc-b14320109d36",
"status": "running",
"connect_path": "/connect?sessionId=2f4484f1-21a2-47e4-80fc-b14320109d36",
"cdp_ws_url": "ws://localhost:9223/devtools/page/87D28712…",
"browser_ws_url": "ws://localhost:9223/devtools/browser/635371e2-…"
}
| Endpoint | Purpose |
|---|---|
POST /api/sessions | Create a browser session |
GET /api/sessions | List sessions |
GET /api/sessions/:id | Session detail |
POST /api/sessions/:id/release | Release the browser and finalize the session |
GET /api/sessions/:id/logs | Per-session structured log entries |
GET /api/sessions/:id/attribution | Typed failure cause (webx why) for a terminal failed session |
6.1 /connect (raw CDP relay) and orchestrator tiers
✅ On this deployment, /connect is available because
WEBX_ORCHESTRATOR=process. Each session owns an isolated Chrome process, so
the authenticated session owner may attach a raw CDP client directly with the
connect_path returned by POST /api/sessions.
Deployments that run the shared-browser context tier deliberately reject
/connect with 409:
{
"error": "gateway_requires_process_tier",
"message": "Raw CDP relay is only available under the process orchestrator tier (set WEBX_ORCHESTRATOR=process). The shared-browser tier cannot expose an isolated browser-level CDP endpoint."
}
That is an isolation guard, not a fault. A context-tier deployment keeps many
sessions inside one shared Chrome, so exposing the browser-level CDP socket
would leak Target.*/Browser.* access across tenants. Use /live (§5) for
visuals under the context tier.
7. Verified execution: receipts, spend, confirmations
Every session carries a tamper-evident, hash-chained receipt ledger:
curl -s -H "x-api-key: $TOKEN" https://webx.agentslab.host/api/sessions/$SID/receipts | jq
{
"mandate_id": "c2910342-9963-4575-b9a7-7368bea9c165",
"budget_limit_minor": 0,
"entries": [],
"verified": true,
"error": null
}
verified: true means the chain re-verified on read. Related endpoints:
| Endpoint | Purpose |
|---|---|
GET /api/sessions/:id/receipts | Hash-chained ledger of approved gated actions |
POST /api/sessions/:id/spend | Budget-bounded spend under the session mandate ({"amount_minor": 500, "payload": {…}}); enforced against WEBX_BUDGET_MINOR, recomputed from the chain on restart |
GET /api/confirmations | Pending operator confirmations for membrane-gated actions |
POST /api/confirmations/:id | `{"approve": true |
Plus operational insight endpoints:
| Endpoint | Purpose |
|---|---|
GET /api/engines | Engine capability profiles |
GET /api/insights/config | Run-config bandit insights |
POST /api/contexts / GET /api/contexts / DELETE /api/contexts/:id | Persistent browser profiles |
POST /api/keys | Issue a per-project API key (returns the raw key once) |
Browser-session create responses include engine, routing_reason, and
redacted routing_attempts. Capability requirements supplied by the caller are
merged with proxy/profile/extension/isolation requirements inferred from the
launch configuration. See multi-engine routing.
8. Error reference
| Status | When | What to do |
|---|---|---|
401 {"error":"unauthorized"} | Missing/invalid API key | Send the key via x-api-key, Bearer, or ?token= |
400 missing sessionId | /live or /connect without sessionId | Add the query param |
404 unknown session | Session released, timed out, or never existed | Sessions expire after their timeout_ms (default 10 min) |
404 on /attribution | Session is not terminal-failed | Attribution is computed for failed runs |
409 gateway_requires_process_tier | /connect on a shared-browser/context-tier deployment | Use /live, or redeploy with WEBX_ORCHESTRATOR=process |
500 internal_error | Unhandled failure starting a task | Check pod logs (§9) |
Task statuses: starting → running → completed | failed | cancelled | timed_out.
9. Operations (this deployment)
Where things live on the server (ssh -i ~/.ssh/poc_server_new root@65.108.78.80):
| Artifact | Path |
|---|---|
| Source tree (rsynced) | /root/webx-src |
| Image build log | /root/webx-build.log |
| K8s manifest | /root/webx-single-node.yaml |
| Master API token | Kubernetes Secret webx-env, key WEBX_API_TOKEN |
Supported container architecture: WebX release images are linux/amd64
only. The pinned Chrome-for-Testing linux64 archives and checksum-verified
Debian _amd64.deb libraries are x86_64; Linux ARM64 is not supported. Both
release Dockerfiles pin every stage, so ARM64 Docker Desktop builds use amd64
emulation instead of mixing architectures.
Deploy a new version:
REV="$(git rev-parse --short=7 HEAD)"
IMAGE_TAG="localhost:32000/webx-server:${REV}-fullchrome"
docker build --platform linux/amd64 -f Dockerfile.ext-engine \
-t "${IMAGE_TAG}" .
docker push "${IMAGE_TAG}"
IMAGE="$(docker image inspect "${IMAGE_TAG}" --format '{{index .RepoDigests 0}}')"
WEBX_RELEASE_REVISION="${REV}" WEBX_RELEASE_IMAGE="${IMAGE}" \
ops/render-webx-single-node.sh > /root/webx-single-node.yaml
microk8s kubectl apply -f /root/webx-single-node.yaml
microk8s kubectl -n webx rollout status deploy/webx-server
Logs / status:
microk8s kubectl -n webx get pods
microk8s kubectl -n webx logs deploy/webx-server -c webx-server --tail=100
Configuration comes from the webx-env and webx-infrastructure Secrets
plus explicit non-secret deployment settings. webx-env must carry
WEBX_API_TOKEN, WEBX_EVIDENCE_ENCRYPTION_KEY,
WEBX_RECEIPT_SIGNING_KEY_ID, WEBX_RECEIPT_SIGNING_KEY, and the provider
credentials. The current process tier sets WEBX_ORCHESTRATOR=process and uses
the full Chrome-for-Testing binary at /opt/chrome/chrome-linux64/chrome; this
is required for extension sessions. WEBX_CDP_HTTP is only used by the
shared-browser tier.
Other knobs the server honors: WEBX_ORCHESTRATOR, WEBX_MAX_CONCURRENT_SESSIONS, WEBX_SESSION_TIMEOUT_MS, WEBX_RUN_MAX_STEPS, WEBX_RUN_MAX_TIMEOUT_SECONDS, WEBX_BUDGET_MINOR, WEBX_MEMBRANE, WEBX_CONFIRM_*, WEBX_DB_URL, WEBX_ALLOWED_ORIGINS, WEBX_ENGINES, WEBX_PROFILES_DIR, WEBX_LOG_*, WEBX_DISCONNECT_GRACE_MS, WEBX_CALLBACK_SIGNING_SECRET, WEBX_DISK_CHECK_PATH, WEBX_DISK_MIN_FREE_BYTES, WEBX_DISK_MIN_FREE_PERCENT, WEBX_WORKFLOW_MEMORY, WEBX_DRIFT_JUDGE, WEBX_ATTRIBUTION_JUDGE.
Run requests may select a model on the effective provider, but a per-run
provider switch is rejected because the request does not carry provider
credentials. Per-run USD budgets are also rejected until model-token metering
is wired end to end. Use the enforced max_steps and timeout_seconds fields;
the deployment ceilings above prevent callers from disabling or inflating
those guards.
Rotate the API token:
NEW=$(openssl rand -hex 24); echo -n "$NEW" > /root/webx-api-token
microk8s kubectl -n webx patch secret webx-env -p "{\"stringData\":{\"WEBX_API_TOKEN\":\"$NEW\"}}"
microk8s kubectl -n webx rollout restart deploy/webx-server
Server quirk: outbound port 80 is blocked on this host. The full-Chrome Dockerfile uses verified HTTPS downloads with pinned checksums and copies the CA bundle into the runtime image. Keep those constraints if you edit it.
Persistence — everything is on the webx-data PVC (/data, microk8s-hostpath), durable across pod restarts:
| Artifact | Where | Backed by |
|---|---|---|
| Sessions, logs, agent trajectory, receipts, usage, quotas, API keys, workflow memory | /data/webx.db (WEBX_DB_URL=sqlite:///data/webx.db) | SQLite, 6 tables |
| Session replay frames | /data/artifacts/replay/<session>/NNNNNN.jpg (WEBX_ARTIFACTS_DIR) | files on the PVC (off-heap, survive restart; oldest 128 sessions retained) |
| Agent screenshots | /data/screenshots (WEBX_SCREENSHOT_DIR) | files on the PVC |
| Persistent context profiles | /data/profiles (WEBX_PROFILES_DIR) | files on the PVC (real Chrome --user-data-dir) |
GET /api/tasks/:id falls back to the store for sessions that predate the current pod, and replay survives a pod restart (verified). The deployment uses strategy: Recreate so two pods never share the volume. Going multi-replica would mean moving the DB to the cluster's PostgreSQL and the blobs to the cluster's MinIO (S3) — the artifact paths are env-configurable for exactly that.
Metrics & dashboards:
- The server exposes Prometheus metrics on a side-channel listener (
WEBX_METRICS_ADDR, default0.0.0.0:9091) — in-cluster only via thewebx-metricsClusterIP Service, never through the public ingress. - Metrics:
webx_http_requests_total{method,path,status},webx_http_request_duration_seconds{path},webx_tasks_started_total,webx_tasks_finished_total{status},webx_active_sessions. - The cluster Prometheus (
monitoringns) scrapes jobwebxevery 15s; the WebX Overview dashboard lives in the existing Grafana athttps://dash.agentslab.host(request rate, p95 latency, task outcomes, active sessions, 5xx, scrape health).
Known limitations:
- Single replica (SQLite store requires it — see above).
- Prometheus alert rules cover availability, server-error ratio, capacity, latency, and outbound callback/integration failures. An operator-owned Alertmanager receiver is still required for paging.
10. Verification record
The full consumer-side verification (scripts + captured outputs) lives in docs/verification/:
| File | What it is |
|---|---|
consumer_test3.py | Final end-to-end consumer test (task + live capture + policy checks) |
ws_events.json | Real captured WebSocket event stream |
verification_report.json | Machine-readable summary of the final verified run |
Verified on 2026-06-12 against https://webx.agentslab.host:
- ✅
GET /health→200 OK(unauthenticated, used by k8s probes) - ✅ Auth gating:
401without key;x-api-key, Bearer, and?token=all accepted - ✅
POST /api/tasks→ agent navigated, answered "Example Domain", validator confirmed — completed in 8 s / 1 step - ✅ REST status reaches
completedwithcompleted_atset - ✅
/wsevent stream:connection.ready → agent.started → agent.observe → agent.action → agent.result → agent.finish - ✅
/liveJPEG screencast (screenshot above is a real captured frame) - ✅
GET /api/sessions/:id/receipts→verified: true - ✅
/connectaccepted under the process tier with the signed session URL - ✅ TLS: Let's Encrypt cert, auto-renewed by cert-manager (expires 2026-09-10, renews ~30 days prior)