Sessions
Create, inspect, mutate, pause, resume and delete sessions.
POST /v1/sessions create
GET /v1/sessions/{id} read
GET /v1/sessions list
PATCH /v1/sessions/{id} update mutable fields
DELETE /v1/sessions/{id} stop and release
POST /v1/sessions/{id}/pause freeze
POST /v1/sessions/{id}/resume unfreeze
POST /v1/sessions/{id}/snapshot capture
GET /v1/sessions/{id}/trace lifecycle, egress and policy events
GET /v1/sessions/{id}/events SSE stream of state changes
Create
curl -sS "$WB_BASE_URL/sessions" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"tier": 2,
"execution_mode": "long_running",
"template": "tpl_dev_polyglot",
"resources": { "vcpu": 2, "memory_mib": 4096, "disk_mib": 20480 },
"region": "bom1",
"policy_id": "pol_build_tools",
"volume_id": "vol_5Km3Xp",
"mount_path": "/workspace",
"env": { "NODE_ENV": "test" },
"secrets": ["sec_github_token"],
"idle_timeout_s": 300,
"max_lifetime_s": 3600,
"end_user_id": "eu_customer_4821",
"labels": { "team": "platform", "purpose": "ci" },
"metadata": { "conversation_id": "conv_912" }
}'
| Field | Required | Default | Notes |
|---|---|---|---|
tier | Yes | — | 0–4. No default, by design |
execution_mode | Yes, on Tiers 0–2 today | — | one_shot or long_running. one_shot currently only differs by rejecting expose_ports; auto-release-on-exit is planned, not yet built |
runtime | No | — | python3.12, python3.11, node22, node20, go, rust — which interpreter kind: code runs under by default |
image | No | — | A container image instead of template/runtime, on Tiers 0–2 today |
expose_ports | No | [] | Up to 4; fixed for the session's lifetime once declared — see Filesystem and ports |
template | No | Tier's platform base | Name or name@digest |
resources | No | Tier's default size class | Rounded up to a size class |
region | No | Project default, or nearest | Subject to residency |
policy_id | No | Project default policy | |
volume_id | No | — | Constrains placement to the volume's zone |
mount_path | No | /workspace | |
restore_from | No | — | A snapshot ID; mutually exclusive with template |
env | No | {} | Non-secret environment variables |
secrets | No | [] | Secret refs; injected at start, never stored in the session |
idle_timeout_s | No | 300 | 30–7200 |
max_lifetime_s | No | 3600 | 60–86400, tier-dependent ceiling |
preemptible | No | false | Discounted rate, evictable with 30 s notice |
end_user_id | No | — | Immutable after creation |
labels, metadata | No | {} | Up to 16 keys each |
browser | Tier 3 only | — | { profile, viewport, user_agent } |
desktop | Tier 4 only | — | { resolution, locale, timezone } |
Returns 201 immediately with state: "provisioning".
{
"id": "sess_2kQ9xwR4",
"object": "session",
"tier": 2,
"execution_mode": "long_running",
"state": "provisioning",
"region": "bom1",
"zone": "bom1-b",
"template_digest": "tpl_dev_polyglot@sha256:9f3c1a…",
"policy_digest": "pol_build_tools@sha256:4b8e2d…",
"resources": { "vcpu": 2, "memory_mib": 4096, "disk_mib": 20480, "size_class": "m" },
"idle_timeout_s": 300,
"max_lifetime_s": 3600,
"end_user_id": "eu_customer_4821",
"created_at": "2026-10-06T09:14:22.061Z",
"connect": {
"exec_url": "https://api.workspaces.build/v1/sessions/sess_2kQ9xwR4/exec",
"pty_url": "wss://connect.workspaces.build/v1/sessions/sess_2kQ9xwR4/pty"
}
}
You can issue exec against a provisioning session immediately — the gateway holds the request
until the session is running. Waiting for running first adds a round trip for nothing.
Read
GET /v1/sessions/sess_2kQ9xwR4?expand=volume,end_user
Adds, once running:
{
"state": "running",
"started_at": "2026-10-06T09:14:22.118Z",
"expires_at": "2026-10-06T10:14:22.118Z",
"last_activity_at": "2026-10-06T09:21:04.882Z",
"cold_start": false,
"start_ms": 61,
"usage_to_date": {
"session_s": 402,
"cpu_ms": 118430,
"mem_gib_ms": 1646592,
"egress_bytes": 33294112,
"estimated_cost_minor": 94,
"currency": "USD"
}
}
usage_to_date is a live estimate from the node's last flush, typically under 15 seconds stale. The
billed figure comes from the metering pipeline.
List
GET /v1/sessions?state=running&tier=2&end_user_id=eu_customer_4821&limit=50
Filters: state (repeatable), tier, region, project_id, end_user_id, label.<key>,
created_after, created_before.
Update
Only three fields are mutable.
curl -sS -X PATCH "$WB_BASE_URL/sessions/$SESSION" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "idle_timeout_s": 900, "labels": { "phase": "deploy" } }'
| Field | Mutable | Note |
|---|---|---|
idle_timeout_s | Yes | Within plan limits; resets the idle timer |
max_lifetime_s | Yes, extend only | Within the tier ceiling |
labels, metadata | Yes | |
| Everything else | No | Tier, template, resources, policy, region, end_user_id are immutable |
Resources are immutable because changing them would mean moving the session to a different host — a different machine with different state. Create a new session instead, from a snapshot.
Delete
curl -sS -X DELETE "$WB_BASE_URL/sessions/$SESSION?snapshot=true&snapshot_name=final" \ -H "Authorization: Bearer $WB_API_KEY"
| Parameter | Default | Effect |
|---|---|---|
snapshot | false | Take a final snapshot before teardown |
snapshot_name | auto | Name for it |
force | false | Skip graceful drain; kills immediately |
Returns 200 with the session in stopping or stopped. Deleting an already-terminal session is a
no-op returning the existing object.
Always delete explicitly. Relying on the idle timeout bills for the whole timeout window. In a 20-step agent loop with a 300-second timeout, that is five extra minutes of compute per run.
Pause and resume
curl -sS -X POST "$WB_BASE_URL/sessions/$SESSION/pause" -H "Authorization: Bearer $WB_API_KEY" curl -sS -X POST "$WB_BASE_URL/sessions/$SESSION/resume" -H "Authorization: Bearer $WB_API_KEY"
Tiers 2–4 only. Memory stays resident on the host, so resume is 150 ms – 1 s, and the session keeps its ID, volume, ports and policy. Maximum pause is one hour, after which the session is stopped.
Billing while paused is paused_gib_month on retained memory and disk — far less than running, far
more than a snapshot. For anything longer than a few minutes, snapshot instead.
Snapshot
curl -sS "$WB_BASE_URL/sessions/$SESSION/snapshot" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "deps-installed", "include_memory": true, "retention_days": 30 }'
The session keeps running. The guest is frozen for 50–200 ms during capture. See snapshots and volumes.
Trace and events
GET /v1/sessions/{id}/trace # complete history, JSON
GET /v1/sessions/{id}/events # SSE, live
event: state_change
data: {"state":"running","cold_start":false,"start_ms":61}
event: egress_denied
data: {"destination":"telemetry.vendor.io","rule":"default_deny"}
event: state_change
data: {"state":"stopping","reason":"client_delete"}
The events stream is how you react to a session failing without polling. See observability.
Lifecycle in practice
const session = await wb.sessions.create({
tier: 2,
template: 'tpl_dev_polyglot',
idleTimeoutS: 120,
endUserId: user.id,
}, { idempotencyKey: `${conversationId}:${turn}:create` })
try {
for (const step of plan.steps) {
const run = await wb.sessions.exec(session.id, { command: step.command })
if (run.exitCode !== 0) throw new StepFailed(step, run)
}
const snap = await wb.sessions.snapshot(session.id, { includeMemory: true })
await store.set(conversationId, snap.id)
} finally {
await wb.sessions.delete(session.id)
}
The finally is the important line. Without it, a thrown exception leaves the session billing until
its idle timeout fires.