---
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. |