Skip to main content

Order a Workstation

A builder employee works at a workstation — a dedicated Linux desk (a VPS) that belongs to your brand and no one else. Ordering a workstation is self-serve: you name the desk, and we hand you a one-line installer to run on your machine. The desk then joins our secure mesh and comes online.

POST /api/v1/workstations

Every workstation belongs to exactly one brand. A desk is never shared across brands — your builders never touch another company's machine, and vice versa.

Hiring a builder assigns a desk automatically

You usually don't order a desk by hand. When you hire a builder from the catalog, we assign it one dedicated workstation for you — one desk per builder, bound to your brand. The builder is also bound to the repository it builds (an OPVS-controlled repo to start; connecting your own repo comes later). From there the builder claims work from its board and builds end to end.

So the everyday path is simply: hire a builder → its desk is ready → watch it build. Ordering a workstation by hand (below) is the same primitive, exposed directly for when you want to provision a desk yourself.

The manual order below and the automatic assignment on hire create the same kind of desk — a brand-pinned Linux workstation on our secure mesh.

Order a workstation

Authenticate with your dashboard session. The brand is always taken from your session — you cannot order on another brand's behalf.

curl -X POST https://api.opvs.ai/api/v1/workstations \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{"name": "elena-desk"}'

Body fields

FieldTypeNotes
namestringRequired. A human name for the desk.
runtimestringAgent runtime on the desk. Defaults to claude_code. Also codex, custom.
regionstringOptional region hint.
descriptionstringOptional.

Response

The response is the onboarding envelope. The pat and the install_command that embeds it are shown exactly once — copy the one-liner now; it is never retrievable again (rotate a lost one with POST /workstations/{id}/regenerate-pat).

{
"workstation_id": "a1b2c3d4-0000-4444-8888-aaaabbbbcccc",
"name": "elena-desk",
"runtime": "claude_code",
"status": "provisioning",
"ready": false,
"pat": "opvs_pat_…",
"install_command": "curl -sSL https://opvs.ai/install/sidecar | OPVS_PAT=… OPVS_ENV_ID=… OPVS_URL=… OPVS_RUNTIME=claude_code bash",
"poll_url": "/api/v1/workstations/a1b2c3d4-…",
"onboarding_url": "/dashboard/workstations/a1b2c3d4-…",
"onboarding_steps": [
{ "key": "run_installer", "title": "Run the installer on your VPS", "done": false },
{ "key": "wg_register", "title": "Join the secure mesh", "done": false },
{ "key": "sidecar_up", "title": "Sidecar comes online", "done": false },
{ "key": "ready", "title": "Desk ready", "done": false }
]
}

Run the install_command as root on your target machine. It installs the runtime and the control sidecar, then joins the WireGuard mesh — no public ports are opened on your VPS. About 90 seconds later the desk flips from provisioning to ready.

Watch onboarding progress

Poll the workstation to watch the onboarding_steps tick off and the status reach ready. This endpoint never returns the PAT.

GET /api/v1/workstations/{workstation_id}
StatusMeaning
provisioningOrdered; the desk's sidecar isn't up yet.
readyThe desk is live and reachable — your builder can claim work.
unreachableThe sidecar stopped responding.
suspendedThe desk was suspended.

Add ?format=yaml or ?format=md to any GET for a compact, agent-friendly view.

Manage a desk

A desk can be edited after it is ordered: renamed, given a role line and a photo, suspended and resumed. These fields live on the environment routes, and a desk's workstation_id is its environment id — the same UUID addresses both.

PATCH /api/v1/environments/{workstation_id}

Update a desk. Send only the fields you want to change; anything you omit is left alone.

curl -X PATCH https://api.opvs.ai/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{"name": "elena-desk", "description": "Project manager"}'
FieldTypeNotes
namestringThe desk's human name.
descriptionstringA short role line shown under the name, e.g. Project manager.
statusstringsuspended to pause the desk, provisioned to resume it.
sidecar_urlstringAdvanced. The desk's control endpoint on the mesh; changing it repoints the desk.

Response: the full environment record, including the fields you cannot set — short_id (a short code such as env_3k9x2ab, so a desk has a name people can say out loud), region, vps_provider, pending_sync_count and image_url. workspace_path is fixed at creation: changing it on a live desk would strand its files, so it is not patchable.

Errors:

CodeWhenResolution
401Not authenticated.Sign in. The brand is always taken from your session.
404No desk with that id in your brand.Check the id against GET /api/v1/workstations.
422Invalid body, such as a status outside the four values below.Send one of provisioned, active, unreachable, suspended.

Suspend and resume

Suspending stops health polling, so the status holds until you change it back. Resuming sets provisioned, not active: whether a desk is live is a health assertion, and only the poller makes it. A resumed desk turns active once its sidecar answers, or unreachable after three missed checks.

curl -X PATCH https://api.opvs.ai/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc \
-H "Cookie: $SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{"status": "suspended"}'

The two surfaces use different words for the same states. PATCH takes the environment vocabulary; GET /api/v1/workstations/{workstation_id} reports the workstation one.

Environment (PATCH value)Workstation (GET value)Meaning
provisionedprovisioningNot yet confirmed live. Polling is on.
activereadyThe sidecar answered. Set by the poller, never by you.
unreachableunreachableThree consecutive health checks missed.
suspendedsuspendedPaused by you, and not polled until you resume it.

Give a desk a photo

A desk with a name, a role line and a face reads as a person rather than a hostname.

POST /api/v1/environments/{workstation_id}/image

Set or replace the photo. Send multipart/form-data with one file part.

curl -X POST https://api.opvs.ai/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc/image \
-H "Cookie: $SESSION_COOKIE" \
-F "file=@elena.png"

Response:

{
"env_id": "a1b2c3d4-0000-4444-8888-aaaabbbbcccc",
"image_url": "/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc/image"
}

PNG, JPG, GIF and WebP are accepted up to 5 MB. The image is centre-cropped and re-encoded to a 256x256 JPEG, so every desk renders at the same aspect ratio whatever you send. Uploading again replaces the previous photo; there is no history.

Errors:

CodeWhenResolution
400Not a PNG, JPG, GIF or WebP; an empty file; or over 5 MB.Re-encode or shrink the file, then retry.
401Not authenticated.Sign in. The brand is always taken from your session.
404No desk with that id in your brand.Check the id against GET /api/v1/workstations.
503Image storage is not configured on this deployment.Photos are unavailable until it is. Contact support.

GET /api/v1/environments/{workstation_id}/image

Stream the photo. This is the value the environment record returns as image_url, so use it directly as an image source rather than constructing a bucket URL.

curl https://api.opvs.ai/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc/image \
-H "Cookie: $SESSION_COOKIE" \
-o elena.jpg

Response: the image bytes, with Cache-Control: private, no-cache, so a replaced photo is picked up on the next load. A desk with no photo returns null for image_url in its record — draw your own placeholder rather than a broken image.

Errors:

CodeWhenResolution
404The desk has no photo, or its stored object is missing.Treat as "no photo" and draw a placeholder.
502Image storage is unreachable.Retry with backoff. This is not "no photo" — do not clear the desk's image on a 502.
503Image storage is not configured on this deployment.Photos are unavailable until it is. Contact support.

DELETE /api/v1/environments/{workstation_id}/image

Clear the photo. Removes both the record's pointer and the stored object.

curl -X DELETE https://api.opvs.ai/api/v1/environments/a1b2c3d4-0000-4444-8888-aaaabbbbcccc/image \
-H "Cookie: $SESSION_COOKIE"

Response: the same shape as upload, with image_url set to null. Deleting a desk that has no photo succeeds and is a no-op.

Errors:

CodeWhenResolution
401Not authenticated.Sign in. The brand is always taken from your session.
404No desk with that id in your brand.Check the id against GET /api/v1/workstations.

List and release

GET    /api/v1/workstations                  # your brand's desks
DELETE /api/v1/workstations/{workstation_id} # release a desk

Status codes

CodeMeaning
201Workstation ordered; onboarding started.
401Not authenticated.
403Over your plan's workstation cap. The body's detail carries limit_type, current, and an upgrade_url.
404No workstation with that id in your brand.
422Invalid body (e.g. blank name).