Credentials the account stores once and binds to computers. Values go in and never come out.
Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
GET/secrets
List secretsmember+
The secrets in one scope: the account-wide ones by default, or one workspace’s with workspace_id. Names, ids and revisions only — no route ever returns a value.
Owners and members may read; viewers cannot, not even the names. An API key confined to a workspace lists that workspace’s secrets, and naming any other scope is a 403. From an account-wide key, a workspace_id that names no workspace of this account (an empty one included) is a 400, No such workspace on this account.
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%/}"'/secrets' \
-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.secrets.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.secrets.list();
console.log(JSON.stringify(result, null, 2));
Whether a secret can be bound to a computer and delivered into it on this platform. While false, secrets can be stored but a binding is refused.
limitsalways
object
Failures
503
Also Secrets are not enabled on this control plane. when this deployment has secrets switched off. That one does not clear by retrying.
POST/secrets
Create a secretowner+
Stores a value under a name, encrypted, for binding to computers with PUT /computers/{id}/secrets. The answer is the secret without its value; the value cannot be read back by any route.
Owners only. A name is unique within its scope: a taken one is a 409 with reason: "name_taken", and sending the same request again gets the same answer.
TWO LIMITS, both answered 400: the account holds at most 100 secrets at once, and deleting one frees a place. A workspace holding active secrets cannot be deleted (see DELETE /workspaces/{id}). Secrets orphaned by older workspace deletions remain unreachable and still count; this safeguard does not repair them. The account may create at most 1000 over its lifetime, deleted ones included, which deleting does not free — once that is reached, only support can raise it. GET /secrets reports both in limits.
Without workspace_id the secret is account-wide, and a computer in any workspace may be bound to it. An API key confined to a workspace creates in that workspace, and naming any other scope is a 403. From an account-wide key, a workspace_id that names no workspace of this account is a 400, No such workspace on this account.
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%/}"'/secrets' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"OPENAI_API_KEY","value":"sk-…"}'
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\":\"OPENAI_API_KEY\",\"value\":\"sk-…\"}")
result = client.secrets.create(body["name"], body["value"])
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":"OPENAI_API_KEY","value":"sk-…"};
const result = await client.secrets.create({name: body.name, value: body.value});
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
Up to 60 characters, no control characters. Leading and trailing spaces are trimmed, and the trimmed name is stored. Unique within the scope, ignoring ASCII case: a name that differs from a taken one only in ASCII case (Token and token) collides, a 409 with reason: "name_taken".
valuerequired
string
The value, as text: 1 to 4096 bytes of UTF-8.
workspace_id
string or null
The workspace it belongs to. Leave it out, or send null, for account-wide.
Response
The secret, without its value.
Response fields
idalways
string
csec- and sixteen hex characters. What a binding names as secret_id.
namealways
string
Its name, as sent with leading and trailing spaces trimmed. Up to 60 characters. Unique within its scope, ignoring ASCII case: names that differ only in ASCII case collide (409, reason: "name_taken").
workspace_idalways
string or null
The workspace it belongs to, or null for one available account-wide.
revision_idalways
string
csr- and twenty-four hex characters. Moves every time the value is replaced; a replace or delete sends back the one it read.
created_atalways
string
RFC 3339 timestamp.
updated_atalways
string
RFC 3339 timestamp of the last replace, or of the create.
last_used_atalways
string or null
When it was last delivered to a computer. null until it has been.
Failures
413
This route reads at most 32 KiB (32768 bytes) of request body, less than the general cap.
503
Also Secrets are not enabled on this control plane. when this deployment has secrets switched off. That one does not clear by retrying.
GET/secrets/{id}
Read a secretmember+
One secret’s name, scope and current revision_id — which a replace or delete needs — without its value. A secret in a workspace is found only with that workspace_id; for any other it is a 404, a workspace_id that names no workspace of this account included. An API key confined to a workspace that names any other scope is refused with 403 instead; an account-wide key gets the 404.
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%/}"'/secrets/csec-0123456789abcdef' \
-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 = "csec-0123456789abcdef"
result = client.secrets.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 = "csec-0123456789abcdef";
const result = await client.secrets.get(id);
console.log(JSON.stringify(result, null, 2));
The workspace the secret belongs to. Leave it out for an account-wide one.
Response
The secret, without its value.
Response fields
idalways
string
csec- and sixteen hex characters. What a binding names as secret_id.
namealways
string
Its name, as sent with leading and trailing spaces trimmed. Up to 60 characters. Unique within its scope, ignoring ASCII case: names that differ only in ASCII case collide (409, reason: "name_taken").
workspace_idalways
string or null
The workspace it belongs to, or null for one available account-wide.
revision_idalways
string
csr- and twenty-four hex characters. Moves every time the value is replaced; a replace or delete sends back the one it read.
created_atalways
string
RFC 3339 timestamp.
updated_atalways
string
RFC 3339 timestamp of the last replace, or of the create.
last_used_atalways
string or null
When it was last delivered to a computer. null until it has been.
Failures
503
Also Secrets are not enabled on this control plane. when this deployment has secrets switched off. That one does not clear by retrying.
PUT/secrets/{id}
Replace a secret’s valueowner+
Replaces the value and moves revision_id. Send the revision_id you read: if it has moved since, the answer is 409 with reason: "stale_revision" and nothing changed. Sending the same request again gets the same answer: read the secret again and retry with its current revision_id.
A computer bound to it as an environment variable gets the new value in its desktop session at its next start or restart; on an image that supports it, a running one is also sent the new value for new shells and for every exec, plain or with session: "desktop", so the next one sees it, while programs already running keep the value they started with. One bound to it as a FILE is also sent the new value while it runs. Both are sent asynchronously and on a best-effort basis: the answer does not wait for that, and does not say whether it landed. Unless the update was confirmed — not if its host was unreachable, or refused it, or took it and never answered — a running computer may still hold the old value until its next start or restart, which always delivers the latest.
A secret a computer’s egress_proxy.credentials_secret_id names must keep a user:password value (a colon, no control characters, one trailing newline ignored): any other is refused with 400 and nothing changed. Owners only.
A secret in a workspace is found only with that workspace_id; for any other it is a 404, a workspace_id that names no workspace of this account included. An API key confined to a workspace that names any other scope, null included, is refused with 403 instead; an account-wide key gets the 404.
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 PUT "${base_url%/}"'/secrets/csec-0123456789abcdef' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"value":"sk-…","revision_id":"csr-0123456789abcdef01234567"}'
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 = "csec-0123456789abcdef"
body = json.loads("{\"value\":\"sk-…\",\"revision_id\":\"csr-0123456789abcdef01234567\"}")
result = client.secrets.replace(id, body["value"], revision_id=body["revision_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 = "csec-0123456789abcdef";
const body = {"value":"sk-…","revision_id":"csr-0123456789abcdef01234567"};
const result = await client.secrets.replace(id, {value: body.value, revisionId: body.revision_id});
console.log(JSON.stringify(result, null, 2));
The workspace the secret belongs to. Leave it out for an account-wide one.
Response
The secret at its new revision, without its value.
Response fields
idalways
string
csec- and sixteen hex characters. What a binding names as secret_id.
namealways
string
Its name, as sent with leading and trailing spaces trimmed. Up to 60 characters. Unique within its scope, ignoring ASCII case: names that differ only in ASCII case collide (409, reason: "name_taken").
workspace_idalways
string or null
The workspace it belongs to, or null for one available account-wide.
revision_idalways
string
csr- and twenty-four hex characters. Moves every time the value is replaced; a replace or delete sends back the one it read.
created_atalways
string
RFC 3339 timestamp.
updated_atalways
string
RFC 3339 timestamp of the last replace, or of the create.
last_used_atalways
string or null
When it was last delivered to a computer. null until it has been.
Failures
413
This route reads at most 32 KiB (32768 bytes) of request body, less than the general cap.
503
Also Secrets are not enabled on this control plane. when this deployment has secrets switched off. That one does not clear by retrying.
DELETE/secrets/{id}
Delete a secretowner+
Deletes it. revision_id is required, and a stale one is a 409 with reason: "stale_revision" and nothing deleted; sending the same request again gets the same answer. Owners only.
Deleting does not recall a value already delivered, and it is not refused while computers are bound to the secret. A running computer bound to it keeps the value it holds. One window remains: if a replace of this secret was still being sent to a computer bound to it as a file when the delete committed, that value may still arrive — a send not yet dispatched is abandoned, one already on its way is not.
Nothing more is delivered after the delete, so a computer still bound to it cannot be given its secrets again. Its next start, and a disk snapshot restored onto it, are refused with 409 and reason: "unsupported" until the binding is removed; a restart, or a reboot inside the guest, stops it rather than bring it up without them; and a suspended one cannot resume its saved session. Remove the binding with PUT /computers/{id}/secrets — before deleting, or on the stopped computer afterwards — to start it again.
AN EGRESS PROXY THAT SIGNS IN WITH IT IS SHUT OFF. Every computer whose egress_proxy.credentials_secret_id names this secret has its proxy credentials revoked on this control plane before the answer is sent, so nothing is sent to its host for it again. Its host is then told to drop the value it holds, without the answer waiting for that: until the host has, new connections can still sign in with it. Once it has, every connection that computer opens is refused rather than sent directly (egress_proxy_pending), until its egress_proxy is saved naming a secret that exists. A host that could not be told is told again by the platform’s next reconcile.
A secret in a workspace is found only with that workspace_id; for any other it is a 404, a workspace_id that names no workspace of this account included. An API key confined to a workspace that names any other scope is refused with 403 instead; an account-wide key gets the 404.
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%/}"'/secrets/csec-0123456789abcdef?revision_id=csr-0123456789abcdef01234567' \
-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 = "csec-0123456789abcdef"
query = {"revision_id":"csr-0123456789abcdef01234567"}
result = client.secrets.delete(id, revision_id=query["revision_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 = "csec-0123456789abcdef";
const query = {"revision_id":"csr-0123456789abcdef01234567"};
const result = await client.secrets.delete(id, {revisionId: query.revision_id});
console.log(JSON.stringify(result, null, 2));