API keys

Who a key is, and minting and revoking keys from a key the dashboard allowed to.

Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.

GET/whoami

Who am Iviewer+

The credential this request was made with: the person holding it, the account and role it acts with, the workspace it is confined to (null for an account-wide key), and the key itself.

The role is the holder’s role now, not when the key was minted. A suspended account may still call this, and account.status says so.

A workspace-scoped key is not told its holder’s email address or name, nor the account’s name, because such a key may be in an end customer’s hands: each is null. Its account.plan is null too; GET /account reports the plan and its ceilings.

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%/}"'/whoami' \
  -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.account.whoami()
    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.account.whoami();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "user": {
    "id": "usr-0123456789abcdef",
    "email": "dana@example.com",
    "name": "Dana Whitfield"
  },
  "account": {
    "id": "acc-d5e6f7a8b9c0d1e2",
    "name": "Acme",
    "plan": "studio",
    "status": "active"
  },
  "role": "owner",
  "workspace": null,
  "key": {
    "id": "key-a1b2c3d4e5f6",
    "name": "ci",
    "prefix": "com_1a2b3c4d…",
    "created_at": "2026-09-20T08:00:00.000Z",
    "last_used_at": "2026-09-25T14:05:00.000Z",
    "workspace_id": null,
    "workspace_name": null,
    "manage_keys": true,
    "minted_by_key_id": null
  }
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Response

The credential and its holder.

Response fields

useralwaysobject
accountalwaysobject
rolealwaysstring

owner, member or viewer: the holder’s role on the account now, which is what the key acts with.

workspacealwaysWorkspace or null
keyalwaysApiKey or null

Failures

403

A suspended account may still make this read: a 403 here is about the credential or the person holding it, never the account’s suspension.

GET/api-keys

List API keysviewer+

Your API keys on this account, newest first. Needs a key with manage_keys on: that is set for a key from the dashboard (Settings → Credentials → API keys → the key’s menu → Allow managing keys), is off for every key until then, and every other key is refused this route with 403.

Only the holder’s own keys, and only those acting on this account. A key confined to a workspace lists the keys confined to that same workspace and no others.

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%/}"'/api-keys' \
  -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.api_keys.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.apiKeys.list();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

[
  {
    "id": "key-a1b2c3d4e5f6",
    "name": "ci",
    "prefix": "com_1a2b3c4d…",
    "created_at": "2026-09-20T08:00:00.000Z",
    "last_used_at": "2026-09-25T14:05:00.000Z",
    "workspace_id": null,
    "workspace_name": null,
    "manage_keys": true,
    "minted_by_key_id": null
  }
]

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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 keys, newest first. The raw keys are never listed.

Response fields

idalwaysstring

key- and twelve hex characters. What an audit entry names as apikey:<id>.

namealwaysstring or null

Its label, up to 60 characters, or null when it was minted without one. For a hosted-MCP (OAuth) credential, which only GET /whoami answers, Connected app: followed by the connected app’s name, up to 100 characters in all.

prefixalwaysstring

The first twelve characters of the key and an ellipsis, for recognising it. Not a secret. oauth for a hosted-MCP (OAuth) credential, as GET /whoami answers it.

created_atalwaysstring

RFC 3339 timestamp.

last_used_atalwaysstring or null

RFC 3339 timestamp of its last authenticated request, or null until it has one.

workspace_idalwaysstring or null

The workspace it is confined to, or null for the whole account.

workspace_namealwaysstring or null

That workspace’s name, or null for an account-wide key.

manage_keysalwaysboolean

Whether this key may use /api-keys. With it a key lists its holder’s keys; creating and revoking also need the holder to be a member or owner, so a viewer’s key can only list. Set only from the dashboard; no key can set it.

minted_by_key_idalwaysstring or null

The id of the API key that minted this one over the API, or null for a key minted from the dashboard. Kept after that key is revoked.

Failures

403

Also the answer, with no reason, to a key that does not have manage_keys on.

POST/api-keys

Create an API keymember+

Mints a key for the key’s holder on this account. The answer carries the raw key in raw, once: it is stored only as a hash and no route returns it again. Needs a key with manage_keys on, and a member or owner role (a viewer mints from the dashboard).

A key cannot mint a key with manage_keys on (403): only the dashboard grants it. Nor can it mint one wider than itself. Without workspace_id a key confined to a workspace mints into that workspace, and naming any other scope, null included, is a 403. An account-wide key may name any workspace of the account (an unknown one is a 400) or none, for another account-wide key.

The body is optional. No body, or one of nothing but whitespace, mints an unnamed key exactly as {} does.

The account holds at most 1000 keys, whoever minted them; the next is a 400.

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%/}"'/api-keys' \
  -H "Authorization: Bearer $MANDALA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-raw '{"name":"ci"}'

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\":\"ci\"}")
    result = client.api_keys.create(name=body["name"])
    print("Key returned once; store result.key securely without logging it.")

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":"ci"};
const result = await client.apiKeys.create({name: body.name});
console.log("Key returned once; store result.key securely without logging it.");

201 Illustrative successful response.

HTTP 201
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "key-0f1e2d3c4b5a",
  "name": "ci-rotation",
  "prefix": "com_01234567…",
  "created_at": "2026-09-25T14:06:00.000Z",
  "last_used_at": null,
  "workspace_id": null,
  "workspace_name": null,
  "manage_keys": false,
  "minted_by_key_id": "key-a1b2c3d4e5f6",
  "raw": "com_0123456789abcdef0123456789abcdef0123456789abcdef"
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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

namestring or null

A label, up to 60 characters; longer is cut; control, bidirectional and invisible formatting characters (such as U+200B and U+2060) are refused (400), while the joiners U+200C and U+200D and the variation selectors (such as U+FE0F) that emoji and some scripts need are allowed. Optional; null is the same as leaving it out.

workspace_idstring or null

The workspace to confine the new key to, or leave it out. See above for a workspace-scoped key.

manage_keysboolean

Must be false or absent. true is refused: only the dashboard grants it.

Response

The key, with the raw key in raw.

Response fields

idalwaysstring

As on ApiKey.

namealwaysstring or null

As on ApiKey.

prefixalwaysstring

As on ApiKey.

created_atalwaysstring

RFC 3339 timestamp.

last_used_atalwaysstring or null

Always null: the key has not been used yet.

workspace_idalwaysstring or null

As on ApiKey.

workspace_namealwaysstring or null

As on ApiKey.

manage_keysalwaysboolean

As on ApiKey. Always false for a key minted over the API.

minted_by_key_idalwaysstring or null

As on ApiKey. Always the id of the key that made this request.

rawalwaysstring

The key: com_ and 48 hex characters. Shown here and never again — store it now. Send it as Authorization: Bearer <raw>.

Failures

403

Also, with no reason: this key does not have manage_keys on; the body asked for manage_keys: true; a workspace-scoped key named another scope; or the key’s holder has not verified their email address (Verify your email before creating an API key.).

413

This route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.

DELETE/api-keys/{id}

Revoke an API keymember+

Revokes one of your keys on this account; anything using it is refused from its next request. Needs a key with manage_keys on, and a member or owner role. A key outside your reach — another person’s, another account’s, or for a workspace-scoped key any key not confined to its workspace — is the same 404 as one that does not exist.

A key may revoke itself; the request that does it is its last.

The SSH keys added through this key go with it: the gateway refuses them to any connection it has not already let through, and each is taken off the computers it was written into as each computer receives the new key list. Until a computer does, a connection already let through can still reach it; the answer does not wait for the computers. No other API key is revoked: the keys it minted keep working, and each names it in minted_by_key_id; the dashboard can revoke them with it, or in one step after it has been revoked.

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%/}"'/api-keys/key-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 = "key-a1b2c3d4e5f6"
    result = client.api_keys.revoke(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 = "key-a1b2c3d4e5f6";
const result = await client.apiKeys.revoke(id);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "ok": true
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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: DELETE, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The API key id.

Response

Revoked.

Response fields

okboolean

Always true. A failure is a status code, not ok: false.

Failures

403

Also the answer, with no reason, to a key that does not have manage_keys on.