primer docs

REST API - workspaces

Workspace providers, templates, and workspace instances make up the three-tier workspace surface under /v1. See the concept and feature pages for background.

Workspace providers: the three backends (local, container, Kubernetes) and how to register them.

Workspace templates: materialisation recipes, init commands, file sources, and lifecycle.

Endpoints

Method Path What it does
POST /v1/workspace_providers Register a workspace backend
GET /v1/workspace_providers List providers
GET /v1/workspace_providers/{id} Fetch one provider
DELETE /v1/workspace_providers/{id} Delete a provider
POST /v1/workspace_providers/find Predicate-based search
POST /v1/workspace_templates Create a template
GET /v1/workspace_templates List templates
GET /v1/workspace_templates/{id} Fetch one template
PUT /v1/workspace_templates/{id} Replace a template
DELETE /v1/workspace_templates/{id} Delete a template
POST /v1/workspace_templates/find Predicate-based search
POST /v1/workspaces Materialise a workspace
GET /v1/workspaces List workspaces
GET /v1/workspaces/{id} Fetch one workspace
PATCH /v1/workspaces/{id} Rename
DELETE /v1/workspaces/{id} Destroy
PUT /v1/workspaces/{id}/files Write a file
GET /v1/workspaces/{id}/files List directory entries
DELETE /v1/workspaces/{id}/files Delete a file or directory
GET /v1/workspaces/{id}/files/read Read a file
GET /v1/workspaces/{id}/files/info Stat a path
GET /v1/workspaces/{id}/files/download Download raw bytes
POST /v1/workspaces/{id}/files/dir Create a directory
GET /v1/workspaces/{id}/log Git commit log
POST /v1/workspaces/{id}/diagnostic Run a diagnostic command
POST /v1/workspaces/{id}/pause Pause (reserved, returns 501)
POST /v1/workspaces/{id}/resume Resume (reserved, returns 501)

POST /v1/workspace_providers

Register a workspace backend. PUT is not defined on this resource; providers are immutable once created.

Local provider

curl -s -X POST https://primer.example/v1/workspace_providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "local-dev",
    "provider": "local",
    "config": {
      "kind": "local",
      "root_path": "~/.primer/workspaces"
    }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_providers",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "local-dev",
        "provider": "local",
        "config": {"kind": "local", "root_path": "~/.primer/workspaces"},
    },
)
r.raise_for_status()
await fetch("/v1/workspace_providers", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "local-dev",
    provider: "local",
    config: { kind: "local", root_path: "~/.primer/workspaces" },
  }),
});

Response 201:

{
  "id": "local-dev",
  "provider": "local",
  "config": { "kind": "local", "root_path": "~/.primer/workspaces" }
}

Container provider

Requires Docker accessible via a Unix socket or TCP endpoint. The provider carries connection + reachability only; image and resource limits go in the template.

curl -s -X POST https://primer.example/v1/workspace_providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "docker-local",
    "provider": "container",
    "config": {
      "kind": "container",
      "runtime": "docker",
      "connection": { "kind": "socket", "socket_path": "/var/run/docker.sock" },
      "reachability": { "kind": "host_port", "bind_host": "127.0.0.1" }
    }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_providers",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "docker-local",
        "provider": "container",
        "config": {
            "kind": "container",
            "runtime": "docker",
            "connection": {"kind": "socket", "socket_path": "/var/run/docker.sock"},
            "reachability": {"kind": "host_port", "bind_host": "127.0.0.1"},
        },
    },
)
r.raise_for_status()
await fetch("/v1/workspace_providers", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "docker-local",
    provider: "container",
    config: {
      kind: "container",
      runtime: "docker",
      connection: { kind: "socket", socket_path: "/var/run/docker.sock" },
      reachability: { kind: "host_port", bind_host: "127.0.0.1" },
    },
  }),
});

Kubernetes provider (in-cluster)

Use connection.kind = "in_cluster" when the primer platform itself runs inside the cluster. Use connection.kind = "kubeconfig" with a path for host-side deployments.

curl -s -X POST https://primer.example/v1/workspace_providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "k8s-prod",
    "provider": "kubernetes",
    "config": {
      "kind": "kubernetes",
      "connection": { "kind": "in_cluster" },
      "namespace": "primer-workspaces",
      "reachability": { "kind": "in_cluster" }
    }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_providers",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "k8s-prod",
        "provider": "kubernetes",
        "config": {
            "kind": "kubernetes",
            "connection": {"kind": "in_cluster"},
            "namespace": "primer-workspaces",
            "reachability": {"kind": "in_cluster"},
        },
    },
)
r.raise_for_status()
await fetch("/v1/workspace_providers", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "k8s-prod",
    provider: "kubernetes",
    config: {
      kind: "kubernetes",
      connection: { kind: "in_cluster" },
      namespace: "primer-workspaces",
      reachability: { kind: "in_cluster" },
    },
  }),
});

POST /v1/workspace_templates

Create a template that describes how to materialise a workspace against a provider.

Local template

curl -s -X POST https://primer.example/v1/workspace_templates \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "local-tpl",
    "description": "Local dev workspace",
    "provider_id": "local-dev",
    "backend": { "kind": "local" }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_templates",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "local-tpl",
        "description": "Local dev workspace",
        "provider_id": "local-dev",
        "backend": {"kind": "local"},
    },
)
r.raise_for_status()
await fetch("/v1/workspace_templates", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "local-tpl",
    description: "Local dev workspace",
    provider_id: "local-dev",
    backend: { kind: "local" },
  }),
});

Container template

curl -s -X POST https://primer.example/v1/workspace_templates \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "container-tpl",
    "description": "Docker runtime workspace",
    "provider_id": "docker-local",
    "backend": {
      "kind": "container",
      "image": "primer/workspace-runtime:1.0"
    }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_templates",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "container-tpl",
        "description": "Docker runtime workspace",
        "provider_id": "docker-local",
        "backend": {"kind": "container", "image": "primer/workspace-runtime:1.0"},
    },
)
r.raise_for_status()
await fetch("/v1/workspace_templates", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "container-tpl",
    description: "Docker runtime workspace",
    provider_id: "docker-local",
    backend: { kind: "container", image: "primer/workspace-runtime:1.0" },
  }),
});

Kubernetes template

curl -s -X POST https://primer.example/v1/workspace_templates \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "k8s-tpl",
    "description": "K8s pod workspace",
    "provider_id": "k8s-prod",
    "backend": {
      "kind": "kubernetes",
      "image": "127.0.0.1:30500/primer/workspace-runtime:1.0",
      "entrypoint": ["python", "-m", "primer_runtime.server"],
      "pvc_size": "1Gi"
    }
  }'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspace_templates",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "k8s-tpl",
        "description": "K8s pod workspace",
        "provider_id": "k8s-prod",
        "backend": {
            "kind": "kubernetes",
            "image": "127.0.0.1:30500/primer/workspace-runtime:1.0",
            "entrypoint": ["python", "-m", "primer_runtime.server"],
            "pvc_size": "1Gi",
        },
    },
)
r.raise_for_status()
await fetch("/v1/workspace_templates", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: "k8s-tpl",
    description: "K8s pod workspace",
    provider_id: "k8s-prod",
    backend: {
      kind: "kubernetes",
      image: "127.0.0.1:30500/primer/workspace-runtime:1.0",
      entrypoint: ["python", "-m", "primer_runtime.server"],
      pvc_size: "1Gi",
    },
  }),
});

POST /v1/workspaces

Materialise a workspace from a template. The server assigns an id of the form ws-<hex> if id is omitted. For container and kubernetes backends, poll GET /v1/workspaces/{id} until phase == "running".

curl -s -X POST https://primer.example/v1/workspaces \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"template_id": "local-tpl"}'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspaces",
    headers={"Authorization": f"Bearer {token}"},
    json={"template_id": "local-tpl"},
)
r.raise_for_status()
ws = r.json()
const r = await fetch("/v1/workspaces", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ template_id: "local-tpl" }),
});
const ws = await r.json();

Response 201:

{
  "id": "ws-a1b2c3d4",
  "name": null,
  "template_id": "local-tpl",
  "phase": "running",
  "created_at": "2026-06-08T12:00:00Z",
  "failure_reason": null,
  "last_probe_ok": false,
  "runtime_meta": {
    "url": "ws://127.0.0.1:5959",
    "token": "**********",
    "mapped_host_port": null,
    "k8s_object_name": null
  },
  "reply_binding": null
}

runtime_meta is always present (it carries the runtime URL plus a bearer token, which is redacted in responses).

phase values: pending, running, failed, terminating. Container and kubernetes workspaces may briefly show pending while booting.


PATCH /v1/workspaces/{id}

Rename a workspace. The id handle is immutable; name is the display label.

curl -s -X PATCH https://primer.example/v1/workspaces/ws-a1b2c3d4 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "My workspace"}'
import httpx
r = httpx.patch(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4",
    headers={"Authorization": f"Bearer {token}"},
    json={"name": "My workspace"},
)
r.raise_for_status()
await fetch("/v1/workspaces/ws-a1b2c3d4", {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "My workspace" }),
});

PUT /v1/workspaces/{id}/files

Write a file. Pass path as a query parameter. Parents are created automatically. Writing to .state/ or .tmp/ is rejected with 400.

curl -s -X PUT "https://primer.example/v1/workspaces/ws-a1b2c3d4/files?path=notes.txt" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content": "hello world", "encoding": "text"}'
import httpx
r = httpx.put(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/files",
    params={"path": "notes.txt"},
    headers={"Authorization": f"Bearer {token}"},
    json={"content": "hello world", "encoding": "text"},
)
r.raise_for_status()  # 204 No Content
await fetch("/v1/workspaces/ws-a1b2c3d4/files?path=notes.txt", {
  method: "PUT",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ content: "hello world", encoding: "text" }),
});

Returns 204 on success. Use encoding: "base64" for binary content.


GET /v1/workspaces/{id}/files/read

Read a file. Returns FileReadResponse.

curl -s "https://primer.example/v1/workspaces/ws-a1b2c3d4/files/read?path=notes.txt&encoding=text" \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.get(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/files/read",
    params={"path": "notes.txt", "encoding": "text"},
    headers={"Authorization": f"Bearer {token}"},
)
data = r.json()
const r = await fetch(
  "/v1/workspaces/ws-a1b2c3d4/files/read?path=notes.txt&encoding=text",
  { headers: { "Authorization": `Bearer ${token}` } },
);
const data = await r.json();

Response 200:

{
  "path": "notes.txt",
  "encoding": "text",
  "content": "hello world",
  "size_bytes": 11
}

GET /v1/workspaces/{id}/files

List a directory. Returns an object with an items array of FileEntry objects.

curl -s "https://primer.example/v1/workspaces/ws-a1b2c3d4/files?path=." \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.get(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/files",
    params={"path": "."},
    headers={"Authorization": f"Bearer {token}"},
)
listing = r.json()
const r = await fetch("/v1/workspaces/ws-a1b2c3d4/files?path=.", {
  headers: { "Authorization": `Bearer ${token}` },
});
const listing = await r.json();

Each FileEntry:

{
  "path": "notes.txt",
  "kind": "file",
  "size_bytes": 11,
  "modified_at": "2026-06-08T12:01:00Z"
}

kind is one of file, dir, or symlink.


DELETE /v1/workspaces/{id}/files

Delete a file or directory. Add ?recursive=true to delete a non-empty directory; without it a non-empty directory returns 400.

curl -s -X DELETE "https://primer.example/v1/workspaces/ws-a1b2c3d4/files?path=old-dir&recursive=true" \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.delete(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/files",
    params={"path": "old-dir", "recursive": "true"},
    headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()  # 204
await fetch("/v1/workspaces/ws-a1b2c3d4/files?path=old-dir&recursive=true", {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${token}` },
});

POST /v1/workspaces/{id}/files/dir

Create a directory. Parent directories are created automatically. Returns 400 if the path already exists.

curl -s -X POST "https://primer.example/v1/workspaces/ws-a1b2c3d4/files/dir?path=src/utils" \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.post(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/files/dir",
    params={"path": "src/utils"},
    headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()  # 204
await fetch("/v1/workspaces/ws-a1b2c3d4/files/dir?path=src/utils", {
  method: "POST",
  headers: { "Authorization": `Bearer ${token}` },
});

POST /v1/workspaces/{id}/diagnostic

Run a diagnostic shell command inside the workspace. The command must start with one of the allowed names: echo, pwd, whoami, uname, ls. Anything else returns 400. timeout_seconds is capped at 30.

curl -s -X POST https://primer.example/v1/workspaces/ws-a1b2c3d4/diagnostic \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"command": "echo hi", "timeout_seconds": 10}'
import httpx
r = httpx.post(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/diagnostic",
    headers={"Authorization": f"Bearer {token}"},
    json={"command": "echo hi", "timeout_seconds": 10},
)
result = r.json()
const r = await fetch("/v1/workspaces/ws-a1b2c3d4/diagnostic", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ command: "echo hi", timeout_seconds: 10 }),
});
const result = await r.json();

Response 200:

{
  "exit_code": 0,
  "stdout": "hi\n",
  "stderr": ""
}

GET /v1/workspaces/{id}/log

Return the git commit log for the workspace's .state repo. Pass ?limit=N to cap results.

curl -s "https://primer.example/v1/workspaces/ws-a1b2c3d4/log?limit=20" \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.get(
    "https://primer.example/v1/workspaces/ws-a1b2c3d4/log",
    params={"limit": 20},
    headers={"Authorization": f"Bearer {token}"},
)
log = r.json()
const r = await fetch("/v1/workspaces/ws-a1b2c3d4/log?limit=20", {
  headers: { "Authorization": `Bearer ${token}` },
});
const log = await r.json();

POST /v1/workspaces/{id}/pause and /resume

Workspace-level pause/resume is reserved in v1. Both routes return 501 with {"detail": {"error": "not_implemented"}}. Session-level pause/resume is available under /v1/workspaces/{id}/sessions/{session_id}/pause.


Error responses

All error responses follow RFC 7807:

{
  "type": "/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "workspace ws-missing not found",
  "instance": "/v1/workspaces/ws-missing"
}

Common status codes: 400 invalid path or argument, 404 resource not found, 422 validation error, 501 not implemented.