WebX Docs
← Catalog Raw Markdown Open console

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.host WebSocket: wss://webx.agentslab.host

Everything below — every response body, event payload, and screenshot — was captured from this deployment on 2026-06-12.


1. Architecture

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:

MethodExample
x-api-key header (recommended)curl -H "x-api-key: $WEBX_TOKEN" …
Authorization: Bearercurl -H "Authorization: Bearer $WEBX_TOKEN" …
?token= query parameterwss://…/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 the webx-env Secret — 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

Task flow

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:

EventMeaning
connection.readySocket accepted; events for your session_id will follow
agent.startedTask accepted, agent loop starting
agent.observeOne perception pass (current URL attached)
agent.actionAn action the agent is executing (navigate, click, fill, …)
agent.resultOutcome of an action or of the whole run (success flag)
agent.finishTerminal 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:

Live view screenshot — example.com rendered in an isolated deployed Chrome process

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).

Playground — light theme

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

Playground — dark theme

What it does:

Admin portal — session detail with live view and log playback


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…"
}
FieldMeaning
url (required)Absolute http(s) URL to render
formatsAny of text, html, links, title, screenshot. Omitted → text+links+title
wait_msExtra 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

5.9 Enterprise identity lifecycle

5.9 Observability, replay, and human-in-the-loop

EndpointWhat it gives you
GET /api/sessions/:id/trajectoryThe agent's step timeline — the persisted observe/act/result events, in order (the "what did the agent do" trace)
GET /api/sessions/:id/replayCaptured screencast frames (base64 JPEG, oldest-first) for post-run replay
GET /replay/:idA standalone HTML replay player (scrub, play, speed) — open https://webx.agentslab.host/replay/<session>?token=<key>
GET /api/sessions/:id/logsPer-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

EndpointWhat it does
GET /api/billing/usagePer-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

Session flow

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-…"
}
EndpointPurpose
POST /api/sessionsCreate a browser session
GET /api/sessionsList sessions
GET /api/sessions/:idSession detail
POST /api/sessions/:id/releaseRelease the browser and finalize the session
GET /api/sessions/:id/logsPer-session structured log entries
GET /api/sessions/:id/attributionTyped 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:

EndpointPurpose
GET /api/sessions/:id/receiptsHash-chained ledger of approved gated actions
POST /api/sessions/:id/spendBudget-bounded spend under the session mandate ({"amount_minor": 500, "payload": {…}}); enforced against WEBX_BUDGET_MINOR, recomputed from the chain on restart
GET /api/confirmationsPending operator confirmations for membrane-gated actions
POST /api/confirmations/:id`{"approve": true

Plus operational insight endpoints:

EndpointPurpose
GET /api/enginesEngine capability profiles
GET /api/insights/configRun-config bandit insights
POST /api/contexts / GET /api/contexts / DELETE /api/contexts/:idPersistent browser profiles
POST /api/keysIssue 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

StatusWhenWhat to do
401 {"error":"unauthorized"}Missing/invalid API keySend the key via x-api-key, Bearer, or ?token=
400 missing sessionId/live or /connect without sessionIdAdd the query param
404 unknown sessionSession released, timed out, or never existedSessions expire after their timeout_ms (default 10 min)
404 on /attributionSession is not terminal-failedAttribution is computed for failed runs
409 gateway_requires_process_tier/connect on a shared-browser/context-tier deploymentUse /live, or redeploy with WEBX_ORCHESTRATOR=process
500 internal_errorUnhandled failure starting a taskCheck 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):

ArtifactPath
Source tree (rsynced)/root/webx-src
Image build log/root/webx-build.log
K8s manifest/root/webx-single-node.yaml
Master API tokenKubernetes 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:

ArtifactWhereBacked 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:

Known limitations:


10. Verification record

The full consumer-side verification (scripts + captured outputs) lives in docs/verification/:

FileWhat it is
consumer_test3.pyFinal end-to-end consumer test (task + live capture + policy checks)
ws_events.jsonReal captured WebSocket event stream
verification_report.jsonMachine-readable summary of the final verified run

Verified on 2026-06-12 against https://webx.agentslab.host: