--- title: "Workspaces" description: "Create, list, and read workspaces on the Obol control plane. A workspace is the tenant boundary." --- A workspace is Obol's tenant boundary: `workspace_id` is on every Postgres row and every Redis key. Keys, policy, connections, approvals, receipts, and usage all hang off one. See [Tenancy and identity](/concepts/tenancy-and-identity) for the model. Every route on this page requires an operator session. See [Authentication](/api-reference/overview#authentication). These routes are scoped to the signed-in operator's own memberships. `GET /workspaces` returns only workspaces the caller belongs to, and `org_id` narrows that set, never widens it. ## Workspace object Workspace id, prefixed `ws_`. The organization that owns the workspace. Human-readable name, unique within the organization. One of `dev`, `stage`, `prod`. Environment is enforced downstream: a dev key can never reach a prod connection. When true, every action outside `workspace.read`, `audit.read`, and `usage.read` answers `409`. The workspace's policy-snapshot version. Incremented when a new snapshot is staged for the gateway. ## Create a workspace `POST /api/v1/workspaces` Requires membership in the target organization. Returns `201`. Creation grants the creator an `owner` membership in the same transaction, so a workspace never exists without someone accountable for it. It also writes version-1 snapshot-publication intent in that transaction, then publishes the snapshot after the commit. ### Body The organization to create in, 1–64 characters. The operator must already be a member of a workspace in this organization. A first-time operator gets their organization from `GET /api/v1/me`, which provisions one. 1–128 characters. Must be unique within the organization. One of `dev`, `stage`, `prod`. ```bash cURL curl -X POST "$OBOL_CONTROL_URL/api/v1/workspaces" \ -H "Authorization: Bearer $OBOL_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{"org_id": "org_acme", "slug": "demo-dev", "env": "dev"}' ``` ```json Response { "id": "ws_01hxyz", "org_id": "org_acme", "slug": "demo-dev", "env": "dev", "frozen": false, "snapshot_version": 1 } ``` ### Errors | Status | Condition | |---|---| | `400` | `env` is not `dev`, `stage`, or `prod`. | | `401` | No session, or the session did not verify. | | `403` | `{"detail": "not a member of that organization"}`. Without this check, `org_id` would be a caller-supplied string that lets anyone plant a workspace in another tenant's organization. | | `409` | `{"detail": "workspace slug already exists in this org"}`. | | `422` | `org_id` or `slug` failed length validation. | A snapshot-publication failure after the commit is not an error the caller can act on and is not surfaced: the workspace exists, its `pending` publication row is durable, and the reconciliation sweep converges it. ## List workspaces `GET /api/v1/workspaces` Returns every workspace the signed-in operator is a member of. ### Query parameters Narrow the result to one organization. Omitting it returns memberships across all organizations. ```bash cURL curl "$OBOL_CONTROL_URL/api/v1/workspaces?org_id=org_acme" \ -H "Authorization: Bearer $OBOL_SESSION_TOKEN" ``` ```json Response { "workspaces": [ { "id": "ws_acme_prod", "org_id": "org_acme", "slug": "acme-prod", "env": "prod", "frozen": false, "snapshot_version": 1 } ] } ``` ### Response Workspace objects. Empty when the operator has no memberships matching the filter. This route is not paginated. ## Get a workspace `GET /api/v1/workspaces/{workspace_id}` Requires the `workspace.read` action, which every role holds. ### Path parameters Workspace id, prefixed `ws_`. ```bash cURL curl "$OBOL_CONTROL_URL/api/v1/workspaces/ws_acme_prod" \ -H "Authorization: Bearer $OBOL_SESSION_TOKEN" ``` ```json Response { "id": "ws_acme_prod", "org_id": "org_acme", "slug": "acme-prod", "env": "prod", "frozen": false, "snapshot_version": 1 } ``` ### Errors | Status | Condition | |---|---| | `401` | No session, or the session did not verify. | | `404` | No such workspace, **or** the operator is not a member. Both answer identically. |