The partitions of the account a key can be confined to, and the people who reach them.
Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
GET/workspaces
List workspacesviewer+
The account’s workspaces, oldest first. A workspace partitions the account’s computers: an API key issued against one reaches that workspace’s computers only, and a computer’s workspace_id names the workspace it is in.
An API key confined to a workspace lists that one workspace and no other. An account-wide key sees them all, whatever its holder’s role.
A member or owner can move a computer with an account-wide credential using PUT /computers/{id}/workspace, or in the dashboard’s Settings → General → Workspace, to another workspace or to none. The computer must be stopped with no saved session, and have no snapshots, no secret bindings and no proxy credentials. Once it has moved, API keys confined to its old workspace get 404 for it and keys confined to the new one can reach it; its events, webhooks included, follow it to the new workspace; and its desktop and share credentials are replaced, so links from before stop working.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/workspaces' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
result = client.workspaces.list()
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const result = await client.workspaces.list();
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Response
The workspaces, oldest first.
Response fields
idalways
string
wsp- and twelve hex characters. What a computer’s workspace_id names.
namealways
string
Its name, unique within the account. Up to 40 characters.
created_atalways
string
RFC 3339 timestamp.
POST/workspaces
Create a workspaceowner+
Makes a workspace on the account. Owners only, with an account-wide API key: a key confined to a workspace is refused (403), because it could otherwise make itself somewhere new to stand.
The name is trimmed, must be 1 to 40 characters with no ASCII control characters (U+0000 to U+001F and U+007F), and must not be one the account already uses; an account holds at most 500 workspaces. Each of those is a 400 with a sentence, as is any field but name.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/workspaces' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"customer-acme"}'
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
body = json.loads("{\"name\":\"customer-acme\"}")
result = client.workspaces.create(body["name"])
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const body = {"name":"customer-acme"};
const result = await client.workspaces.create({name: body.name});
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Request body
namerequired
string
Its name: 1 to 40 characters after trimming, unique within the account.
Response
The new workspace.
Response fields
idalways
string
wsp- and twelve hex characters. What a computer’s workspace_id names.
namealways
string
Its name, unique within the account. Up to 40 characters.
created_atalways
string
RFC 3339 timestamp.
Failures
403
Also the answer, with no reason, to any API key confined to a workspace.
413
This route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.
GET/workspaces/{id}
Get a workspaceviewer+
One workspace. An id this credential cannot see is a 404, the same as one that does not exist: another account’s, and — for an API key confined to a workspace — any workspace but its own.
A computer can carry the workspace_id of a workspace that has since been deleted, if the workspace was deleted while it still held computers, before that was refused; if a create or a snapshot clone into it was under way when it was deleted; or if it is a clone (POST /computers/{id}/clone) of such a computer, which keeps its source’s workspace_id. That id answers 404 here. One thing about such a computer is handled: its egress credentials resolve account-wide only, so an egress_proxy naming one of that workspace’s own secrets is held closed (egress_proxy_pendingtrue) until it is saved again naming an account-wide secret. Such a computer can be moved to a live workspace, or to none, with PUT /computers/{id}/workspace or from the dashboard once its secret bindings and proxy credentials are removed and it meets the other conditions under GET /workspaces.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/workspaces/wsp-a1b2c3d4e5f6' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
id = "wsp-a1b2c3d4e5f6"
result = client.workspaces.get(id)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "wsp-a1b2c3d4e5f6";
const result = await client.workspaces.get(id);
console.log(JSON.stringify(result, null, 2));
wsp- and twelve hex characters. What a computer’s workspace_id names.
namealways
string
Its name, unique within the account. Up to 40 characters.
created_atalways
string
RFC 3339 timestamp.
PATCH/workspaces/{id}
Rename a workspaceowner+
Renames a workspace. Its id does not change, so the API keys confined to it and the computers in it are untouched. Owners only, with an account-wide API key: a key confined to a workspace is refused (403), its own workspace included.
The name follows the rules a create does, and a name the account already uses is a 400, as is any field but name. An id this credential cannot see is a 404, the same as one that does not exist.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X PATCH "${base_url%/}"'/workspaces/wsp-a1b2c3d4e5f6' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"customer-acme-prod"}'
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
id = "wsp-a1b2c3d4e5f6"
body = json.loads("{\"name\":\"customer-acme-prod\"}")
result = client.workspaces.rename(id, body["name"])
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "wsp-a1b2c3d4e5f6";
const body = {"name":"customer-acme-prod"};
const result = await client.workspaces.rename(id, body.name);
console.log(JSON.stringify(result, null, 2));
The new name: 1 to 40 characters after trimming, unique within the account.
Response
The workspace, renamed.
Response fields
idalways
string
wsp- and twelve hex characters. What a computer’s workspace_id names.
namealways
string
Its name, unique within the account. Up to 40 characters.
created_atalways
string
RFC 3339 timestamp.
Failures
403
Also the answer, with no reason, to any API key confined to a workspace.
413
This route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.
DELETE/workspaces/{id}
Delete a workspaceowner+
Deletes an empty workspace, and in the same step REVOKES every API key confined to it, whoever holds them: a key has nothing left to reach once its workspace is gone, and it is never widened to the account instead. The answer says how many were revoked. Owners only, with an account-wide API key: a key confined to a workspace is refused (403), its own workspace included.
Webhook subscriptions confined to the workspace (those created with a key scoped to it) are deleted with it in the same step, with every delivery record they hold, pending ones included, as DELETE /webhooks/{id} does; the answer does not count them. Account-wide subscriptions are not affected.
A workspace that still holds computers cannot be deleted: the answer is 409 with reason: "unsupported", saying how many, and nothing is deleted or revoked. The count includes a computer whose unfinished workspace change is moving it into this workspace. Move those computers with PUT /computers/{id}/workspace or in the dashboard, or delete them, first. When the platform cannot confirm the workspace is empty because part of it did not answer, the answer is 503, also with nothing deleted; try again shortly.
A workspace holding active secrets also answers 409 with reason: "unsupported", saying how many. Delete those secrets explicitly with DELETE /secrets/{id} first, using their workspace_id and current revision_id, then retry. The refusal preserves the workspace, its keys and all secret data. Deleted secrets do not block workspace deletion. Deleting a secret frees an active slot but does not reduce the lifetime count of 1000 created secrets.
This safeguard prevents new orphan secrets; it does not repair secrets orphaned by older workspace deletions. Those remain unreachable: listing or creating with a deleted workspace_id is a 400, and reading, replacing or deleting one of its secrets is a 404. They still count toward the 100 active secrets per account.
Snapshots taken in it are kept. An account-wide key or the dashboard can still clone them: a clone built from the disk (a disk snapshot, or memory: false) lands in no workspace, and one that would resume a memory snapshot’s session is refused with 409 (see POST /snapshots/{id}/clone).
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X DELETE "${base_url%/}"'/workspaces/wsp-a1b2c3d4e5f6' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
id = "wsp-a1b2c3d4e5f6"
result = client.workspaces.delete(id)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "wsp-a1b2c3d4e5f6";
const result = await client.workspaces.delete(id);
console.log(JSON.stringify(result, null, 2));
409 Move or delete its computers first; nothing was changed.
HTTP 409
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
{
"error": "This workspace still holds 2 computers. Move or delete them first: a workspace that holds computers cannot be deleted.",
"reason": "unsupported",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
409 Delete the active secrets first; the workspace, keys and secret data are preserved.
HTTP 409
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
{
"error": "This workspace still holds 1 secret. Delete it first: a workspace that holds secrets cannot be deleted.",
"reason": "unsupported",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Path parameters
idrequired
string
The workspace id.
Response
Deleted, with the count of keys it revoked.
Response fields
okalways
boolean
Always true. A failure is a status code, not ok: false.
revoked_keysalways
integer
How many API keys were confined to the workspace and were revoked with it, whoever held them.
Failures
403
Also the answer, with no reason, to any API key confined to a workspace.
409
The workspace still holds computers or active secrets, with reason: "unsupported" and a count. Computers include any that an unfinished workspace change is moving into it. Nothing was deleted: no key was revoked and no webhook subscription removed. Move or delete its computers and explicitly delete its secrets before retrying.
503
Whether the workspace holds computers could not be confirmed. Nothing was deleted.
GET/workspaces/{id}/members
List a workspace’s membersviewer+
The account’s accepted members, oldest member first. Workspaces do not divide people: members share every workspace at their account role, subject to seat and sign-in suspension. Outstanding invitations not yet accepted are left out. A member waiting for a seat, because the plan has fewer seats than the account has people, is listed with seat_suspended: true and cannot act on the account until their seat is restored. suspended reports global sign-in suspension independently; either or both flags can be true.
An API key confined to a workspace cannot list members (403), because the list is the whole account’s. Use an account-wide key.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/workspaces/wsp-a1b2c3d4e5f6/members' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with Client(api_key=os.environ["MANDALA_API_KEY"], base_url=os.environ.get("MANDALA_BASE_URL") or "https://app.mandala.computer/api/v1") as client:
id = "wsp-a1b2c3d4e5f6"
result = client.workspaces.members(id)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "wsp-a1b2c3d4e5f6";
const result = await client.workspaces.members(id);
console.log(JSON.stringify(result, null, 2));
Their display name, or null when they have not set one.
rolealways
string
owner, member or viewer — their role on the account, which is their role in every workspace.
accepted_atalways
string
RFC 3339 timestamp of when they joined the account.
suspendedalways
boolean
true for global sign-in suspension. They are still on the account. This is independent of seat_suspended; either or both flags can be true.
seat_suspendedalways
boolean
true when this member is waiting for a seat on this account and cannot act on it. This is independent of sign-in suspension (suspended) and returns to false when their seat is restored.
Failures
403
Also the answer to any API key confined to a workspace, with no reason: the members are the whole account’s.