Sessions

A live, headful Chrome on a real Linux desktop. The product.

A session is a disposable Chrome browser running in its own Kubernetes Pod, on its own user-namespace, with its own desktop environment. You drive it from your code over CDP and (optionally) watch it over a live-view URL.

Lifecycle

QUEUED  →  PROVISIONING  →  RUNNING  →  COMPLETED   (or EXPIRED / ERRORED)

States are UPPERCASE on the wire (Python enum carries over to JSON verbatim); the dashboard renders them as title-case labels ("Running", "Released"). "Released" in the UI maps to COMPLETED on the API.

API statusUI labelMeaningBillable?
QUEUEDQueuedRequest accepted; pool finding capacity. Usually < 1 s.No
PROVISIONINGProvisioningPod starting; Chrome launching. Usually 3–8 s.No
RUNNINGRunningCDP reachable; the browser is yours.Yes
COMPLETEDReleasedOwner called PATCH /v1/sessions/{id} {"status":"REQUEST_RELEASE"}. Pod gone.No (final tick rounded up to the second)
EXPIREDExpiredWe hit the session's max lifetime (default 30 min). Pod auto-destroyed.Yes (until expiry)
ERROREDErroredPod crashed, agent disconnected, or the runtime image failed to start.Refunded on next invoice.

State transitions are append-only; you can replay a session's history in the audit log.

Request shape

Every POST /v1/sessions accepts (all fields optional unless noted):

{
  "region":            "us-east1",
  "runtime_kind":      "chromium",
  "timeout_seconds":   1800,
  "keep_alive_seconds": 0,
  "viewport":          { "width": 1280, "height": 720 },
  "user_agent":        "Mozilla/5.0 …",
  "proxy":             { "url": "http://…", "username": "…", "password": "…" },
  "profile_id":        "01J…",
  "storage_state":     { "cookies": [], "origins": [] },
  "labels":            { "tenant_dept": "rcm" },
  "recording":         { "enabled": true }
}
  • runtime_kind"chromium" (default) or "rda" (Windows

RDA runtime; the storage_state field is rejected for rda).

  • timeout_seconds — hard deadline; session auto-releases at

expires_at. Range 60–86400, default 1800.

  • keep_alive_seconds — if > 0, the session does NOT release

when CDP disconnects; it waits for an explicit REQUEST_RELEASE or expires_at. If 0, the session releases when CDP closes.

  • profile_id vs storage_state — pass one or the other.

If both are set, inline storage_state wins. A profile is a named, versioned storage_state you minted earlier with POST /v1/profiles.

  • recording: {"enabled": true} — when set, the agent records

the session and writes a recording artifact (governed by the tenant's recording_retention_days policy; default 30 days, max 3650 = 7 years per HIPAA §164.316(b)(2)).

  • labels — free-form key/value strings, surfaced in audit

dashboards. Do not put PHI here; labels are not scrubbed.

Required headers:

  • Authorization: Bearer <api_key> — workspace-scoped.
  • Idempotency-Key: <uuid> — required; see Idempotency.
  • Content-Type: application/json.

The response is 202 Accepted with the session in QUEUED state; poll GET /v1/sessions/{id} (or use the SDK helpers) until it reaches RUNNING.

Idempotency

Idempotency-Key is mandatory on session creation. We dedupe per workspace, per key, within a 24-hour rolling window:

  • First call with key K: creates the session, returns 202 Accepted.
  • Subsequent calls with the same K and any payload (within 24 h):

returns the original session and 200 OK.

  • After 24 h, the key is free to reuse.

This protects you against the classic dual-write hazard: client times out the HTTP request, retries, ends up with two sessions, one unreferenced and billing in the background. Don't try to "be smart" by omitting the header — we return 400 Bad Request if it's missing.

Connect URL

The connect_url is a wss://api.sessions.ventus.ai/v1/sessions/<id>/cdp?token=<jwt>.

  • Works with any CDP-aware client: Playwright (connectOverCDP),

Puppeteer (connect), pyppeteer, chromedp, raw websocket.

  • The JWT is good for the session's lifetime — no need to refresh

mid-session.

  • It carries the workspace + session ID as claims, signed by us. You

can't replay it against a different session.

Live view

Live view is not yet implemented (v0.2). The API returns a signed live_view_url field, but the manager has no handler for the /v/{session_id} path and the runtime image does not run a KasmVNC server (runtime-image/Dockerfile line 4: "KasmVNC integration… is deferred to v0.2"). Treat the field as a placeholder; do not expose the URL in your UI yet. CDP via connect_url works today; live view is on the next milestone.

When live view ships, the live_view_url will open a KasmVNC viewer in the browser — same desktop the automation is driving, in real time, with audio. The viewer JWT will be short-lived (5 min) and read-only by default; interactive handoff (CAPTCHA, MFA) will be opt-in.

Regions

Today, all sessions schedule in us-east1. The API accepts a region string and us-east1 is the only value that produces a running session. Multi-region (us-west2, europe-west1) and per-region data-residency enforcement are on the roadmap; they are not enforced today, so do not rely on a non-us-east1 value for compliance.

Keep-alive and timeout

Default session timeout is 30 minutes of wall-clock from PROVISIONING (timeout_seconds default 1800, max 86400). To extend:

curl -X POST "https://api.sessions.ventus.ai/v1/sessions/$ID:keepalive" \
  -H "Authorization: Bearer $VENTUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"extend_seconds": 600}'

extend_seconds defaults to 600 (10 min); the request validator caps it at 86400 (24 hours) per call. Multiple keep-alives within a single session are allowed; each one rolls the expiry forward by extend_seconds. There is no separate session-lifetime ceiling beyond the timeout_seconds you set at create time.

Release

Tenant-initiated release (you own the session):

curl -X PATCH "https://api.sessions.ventus.ai/v1/sessions/$ID" \
  -H "Authorization: Bearer $VENTUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"REQUEST_RELEASE"}'

Returns the session with status: "COMPLETED", released_at: <iso>, and release_reason: "client_release".

Admin force-stop (operator surface, separate audit class):

curl -X DELETE "https://api.sessions.ventus.ai/v1/sessions/$ID" \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "X-Reason: $REASON"   # ≥ 8 chars, recorded in audit

The two paths exist because the audit log distinguishes "owner cleaned up after themselves" from "operator forcibly terminated someone's session". Both record the actor; only DELETE requires X-Reason.

Sessions and PHI

Sessions are the surface where PHI flows. What's in place today:

  1. No persistent disk. Every session gets an emptyDir mount;

nothing survives Pod termination.

  1. Sensitive-key redaction on event/log emit. domain/scrubber.py

redacts a fixed allowlist of fields (authorization, cookie, token, secret, api_key, connect_url, live_view_url, recording_url, vnc_password, storage_state, …) on the Redis event bus and structured-log surfaces. This is token / secret redaction, not content-pattern PHI scrubbing — a content-pattern PHI scrubber (SSN, MRN, payer-bill, etc.) is on the roadmap and not in v0.4.

  1. Audit captures who, not what. The audit log records "user U

started session S in workspace W at time T" but never the URLs S visited or the response payloads it processed. detail is a tight, allowlist-only JSONB object.

The practical implication: don't log PHI from your own automation code. We can't pattern-scrub what we receive; we only redact the known auth/secret keys. PHI scrubbing of free-form text is a defense in depth we plan to add, not one to rely on yet.

If you need to debug an actual session's network activity, use the session's own Chrome DevTools record/replay on your client; don't ask Ventus support to surface body content from our logs, because there isn't any.