API design principles
Conventions every endpoint follows — versioning, resources, actions, streaming, and the rules we committed to.
https://api.workspaces.build/v1 control calls wss://connect.workspaces.build/v1 streams https://*.run.workspaces.build exposed session ports
JSON over HTTPS. Server-sent events for run output. WebSocket for PTY, CDP and desktop control. WebRTC for desktop media.
The machine-readable contract is openapi/workspaces-build.yaml. It is the source
of truth: the gateway validates against it, the SDKs are generated from it, and the published
reference is rendered from it. Prose and spec disagreeing is a build failure, not a documentation
bug.
Principles
1. Resources are nouns, verbs are sub-resources
POST /v1/sessions create
GET /v1/sessions/{id} read
PATCH /v1/sessions/{id} update mutable fields
DELETE /v1/sessions/{id} stop and release
GET /v1/sessions list
POST /v1/sessions/{id}/pause action
POST /v1/sessions/{id}/resume action
POST /v1/sessions/{id}/snapshot action
POST /v1/sessions/{id}/exec action that produces a Run
Actions are POST to a sub-path, never a ?action= parameter and never a verb in the collection
name. If an action produces something durable, it returns that object — exec returns a Run.
2. The tier is explicit on every create
There is no default tier. POST /sessions and POST /runs both require tier. A default would
mean a caller could change what boundary their code runs in by changing nothing, which is exactly
the surprise the tier model exists to prevent.
3. Creates return immediately
POST /sessions returns 201 with state: "provisioning" as soon as placement succeeds, typically
within 80 ms. The caller then has three options, and all three are supported first-class:
poll GET /sessions/{id} until state is running
webhook session.running
just stream POST /sessions/{id}/exec — the gateway holds the stream until ready
The third is the recommended one. Waiting for running before issuing the first exec adds a round
trip to every session for no benefit.
4. Everything mutating is idempotent
Every unsafe method accepts Idempotency-Key. A retry with the same key returns the original
response rather than creating a second session. This is not optional politeness — agent loops retry,
and an unsafe retry on a session create is a duplicated bill.
5. Errors are RFC 9457 problem documents
{
"type": "https://docs.workspaces.build/api/errors#quota_exceeded",
"title": "Concurrency quota exceeded",
"status": 429,
"detail": "Project proj_7fTn21 is at its tier 2 concurrency limit of 50.",
"code": "quota_exceeded",
"request_id": "req_9Km2Xp",
"retry_after_s": 12,
"meta": { "tier": 2, "limit": 50, "current": 50 }
}
code is the stable string to branch on. type is a link to the documentation. detail is for a
human and may change. See errors.
6. Lists are cursor-paginated and consistently shaped
{
"object": "list",
"data": [ … ],
"has_more": true,
"next_cursor": "eyJ0IjoxNzU5NzQ…"
}
No offset pagination anywhere. Offsets skip and duplicate rows under concurrent writes, which for a list of sessions is constant.
7. Streams are first-class, not an afterthought
Long-running output is streamed by default, not polled. An agent watching a build should see output as it appears.
POST /sessions/{id}/exec?stream=true SSE: stdout, stderr, exit
GET /sessions/{id}/events SSE: lifecycle transitions
WS /v1/sessions/{id}/pty interactive terminal
WS /v1/sessions/{id}/cdp Chrome DevTools Protocol
8. Usage is on the object
Every session and run carries a usage block. A caller never has to join against a separate billing
API to answer "what did this cost".
9. Attribution is one field
end_user_id on any create tags the resulting usage. Nothing else is required to get per-customer
metering, quotas and spend caps.
10. Nothing is implicit about money or isolation
No automatic tier escalation. No automatic resource growth. No automatic retry that creates a second billable session. No silent region fallback. Every one of those would be convenient and every one would surprise someone with a bill or a weakened boundary.
Versioning
The major version is in the path: /v1. It changes only for a break with no migration path, and the
previous major is supported for at least 12 months after the next is generally available.
Additive changes land in /v1 continuously: new fields, new endpoints, new enum values, new
optional parameters. Clients must tolerate unknown fields and unknown enum values.
Behavioural changes that are not additive are gated by a dated version header:
WB-Version: 2026-10-01
Omitting it pins to the version in effect when the API key was created, so an integration does not change under a customer who has not asked it to. Dated versions are listed in the decision log.
Removing a field, renaming a field, narrowing a type, removing an enum value, changing a default, changing an error code for an existing condition, or making an optional field required. Adding any of those is not breaking. Changing a meter name is breaking even though no endpoint signature changes, because it breaks every customer's cost reporting.
Request conventions
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer … | Yes | Secret key, publishable key or session token |
Content-Type: application/json | On bodies | |
Idempotency-Key | Recommended on unsafe methods | |
WB-Version | No | Pins behavioural version |
WB-Project | No | Selects the project when a key is org-scoped |
Accept: text/event-stream | For SSE | Equivalent to ?stream=true |
| Response header | Always | Purpose |
|---|---|---|
X-Request-Id | Yes | Quote in support requests |
RateLimit-* | Yes | Rate limits |
WB-Version | Yes | The version actually applied |
Field conventions
snake_casekeys everywhere. SDKs map to the host language's idiom.- Timestamps are RFC 3339 UTC with millisecond precision, suffixed
_at. - Durations are integers with the unit in the name:
_ms,_s. - Sizes use binary units with the unit in the name:
_mib,_gib,_bytes. - Money is integer minor units plus
currency, suffixed_minor. No floats on a billing path. - Every object carries
objectwith its type name, so a heterogeneous list is self-describing. - Nullable means "not applicable" or "not yet known"; absent means the field does not exist at this version. They are different and clients should treat them differently.
Expansion
Related objects are IDs by default and expandable on demand.
GET /v1/sessions/sess_2kQ9xwR4?expand=volume,policy,end_user
Expansion is one level deep and limited to four fields per request, because deeper expansion turns one query into an unbounded fan-out.
Test mode
A wb_sk_test_… key runs the full API against simulated compute: real objects, real state machines,
real webhooks, real usage events — no sandboxes and no charges. Start latency is simulated per tier
so a test integration experiences the same ordering and timing shape as production.
Test and live data are completely separate. A test key cannot read a live session and vice versa.