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 status | UI label | Meaning | Billable? |
|---|---|---|---|
QUEUED | Queued | Request accepted; pool finding capacity. Usually < 1 s. | No |
PROVISIONING | Provisioning | Pod starting; Chrome launching. Usually 3–8 s. | No |
RUNNING | Running | CDP reachable; the browser is yours. | Yes |
COMPLETED | Released | Owner called PATCH /v1/sessions/{id} {"status":"REQUEST_RELEASE"}. Pod gone. | No (final tick rounded up to the second) |
EXPIRED | Expired | We hit the session's max lifetime (default 30 min). Pod auto-destroyed. | Yes (until expiry) |
ERRORED | Errored | Pod 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_idvsstorage_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, returns202 Accepted. - Subsequent calls with the same
Kand 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 auditThe 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:
- No persistent disk. Every session gets an
emptyDirmount;
nothing survives Pod termination.
- 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.
- 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.