PLATINUM DOCS

API keys & auth

Mint, scope, list, and revoke the bearer API keys that authenticate every call.

API keys authenticate every API call. Each key belongs to one org and sees only that org's resources. Use a key from the SDKs, the CLI, CI, or raw REST.

Authenticate

Do not put the key in a ?token= query parameter. The one exception is the /terminal WebSocket, where browsers cannot set headers. Send the key as a bearer token in the Authorization header on every request.

CredentialFormUsed by
API keyAuthorization: Bearer pt_live_…SDKs, CLI, CI, curl
Session cookieclient.session_token, set at dashboard sign-inthe dashboard

The SDKs have no typed wrapper for key management. Use client.request() for those calls, as shown below.

Check what a credential resolves to:

import { Platinum } from "@platinum-dev/sdk";

const client = new Platinum({ url: process.env.PT_API_URL!, token: process.env.PT_TOKEN! });
const me = await client.me();
from platinum import Platinum

client = Platinum()
me = client.me()
pt whoami
curl -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/me

GET /v1/me needs a full-access (*) key or a session. A scoped key uses GET /v1/auth/me instead.

Create a key

You can also mint from the dashboard → API keys.

The token appears once, at mint. It never appears again — not in the dashboard, not in the key list. Store it immediately.

const key = await client.request("POST", "/v1/auth/api-keys", {
  name: "ci-bot",
  scopes: ["sandboxes:*"],
  expires_at: "2026-10-15T00:00:00Z",
});
key = client.request("POST", "/v1/auth/api-keys", {
    "name": "ci-bot",
    "scopes": ["sandboxes:*"],
    "expires_at": "2026-10-15T00:00:00Z",
})
pt key create --name ci-bot --scope "sandboxes:*" --expires-in 90
curl -X POST $PT_API_URL/v1/auth/api-keys \
  -H "Authorization: Bearer $PT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ci-bot", "scopes": ["sandboxes:*"], "expires_at": "2026-10-15T00:00:00Z" }'
FieldRules
namerequired, 1–120 chars
scopesoptional string[], defaults to ["*"]
expires_atoptional ISO 8601, must be in the future; omit for a key that never expires

For local dev, pt login mints a key and stores it in your OS keychain. In CI, set PLATINUM_API_KEY to override it.

Scope a key

Scopes limit what a key can touch. Only user-minted keys carry scopes. Dashboard sessions are unscoped. Four resources are scopeable, each matching its /v1/<resource> route group:

ResourceCovers
sandboxesCreate and drive sandboxes — sandboxes
templatesBuild and version templates — templates
volumesPersistent disks to mount — volumes
appsLong-running deployments
Scope tokenGrants
*Everything (the default)
<resource>:readRead-only (GET/HEAD)
<resource>:writeFull access, implies read

<resource> and <resource>:* are aliases for :write. Account-level routes — auth, billing, org, audit, key management — require *. The one exception is GET /v1/auth/me.

List the full scope model with pt key scopes.

Scope a key to one sandbox

A sandbox:<id> scope confines a key to one sandbox and its direct children. The key can do nothing else in the organization.

Use this to give an agent running inside a sandbox its own credential. Inject the key when you create the sandbox. The agent can then drive its own sandbox and create child sandboxes, and it cannot reach any other sandbox you own.

A sandbox cannot mint a key for itself. You create the key and inject it.

// The SDK has no key-management surface. Mint the key with the CLI or REST below,
// then inject the token when you create the child sandbox.
await client.sandboxes.create({ template: "pt-base", parent_id: sbx.id, env: { PT_TOKEN: scopedToken } });
# The SDK has no key-management surface. Mint the key with the CLI or REST below,
# then inject the token when you create the child sandbox.
client.sandboxes.create(template="pt-base", parent_id=sbx["id"], env={"PT_TOKEN": scoped_token})
pt key create --name agent --scope "sandbox:sbx_01K…"
curl -X POST "$PT_API_URL/v1/auth/api-keys" -H "Authorization: Bearer $PT_TOKEN" \
  -H 'Content-Type: application/json' -d '{ "name": "agent", "scopes": ["sandbox:sbx_01K…"] }'

The key grants these, and nothing more:

RequestResult
Any call on the scoped sandboxAllowed
Any call on a direct child of itAllowed
Create a sandboxAllowed, as a child of the scoped sandbox
Any call on another sandbox403 sandbox_out_of_scope
Templates, volumes, apps403 — add those scopes separately

Warning: the scope covers one level. A grandchild is out of scope.

Listing sandboxes with this key returns only the scoped sandbox and its children.

sandbox:* is rejected. Organization-wide sandbox access is the sandboxes scope.

List keys

List rows carry id, prefix, name, scopes, createdAt, lastUsedAt, expiresAt, and revokedAt — never the token. The prefix is the leading part of the key. Use it to tell keys apart.

const keys = await client.request("GET", "/v1/auth/api-keys");
keys = client.request("GET", "/v1/auth/api-keys")
pt key list
curl -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/auth/api-keys

Revoke a key

Revocation is immediate. The next request with a revoked key fails. To rotate a key: mint a new key, move traffic over, then revoke the old key.

await client.request("DELETE", "/v1/auth/api-keys/key_123");
client.request("DELETE", "/v1/auth/api-keys/key_123")
pt key rm key_123
curl -X DELETE -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/auth/api-keys/key_123

Keys belong to the org. Any member can list, mint, and revoke them. Every mint and revocation lands in the audit log as apikey.created / apikey.revoked.

Full request/response shapes: API reference.