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.
| Credential | Form | Used by |
|---|---|---|
| API key | Authorization: Bearer pt_live_… | SDKs, CLI, CI, curl |
| Session cookie | client.session_token, set at dashboard sign-in | the 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 whoamicurl -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/meGET /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 90curl -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" }'| Field | Rules |
|---|---|
name | required, 1–120 chars |
scopes | optional string[], defaults to ["*"] |
expires_at | optional 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:
| Resource | Covers |
|---|---|
sandboxes | Create and drive sandboxes — sandboxes |
templates | Build and version templates — templates |
volumes | Persistent disks to mount — volumes |
apps | Long-running deployments |
| Scope token | Grants |
|---|---|
* | Everything (the default) |
<resource>:read | Read-only (GET/HEAD) |
<resource>:write | Full 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:
| Request | Result |
|---|---|
| Any call on the scoped sandbox | Allowed |
| Any call on a direct child of it | Allowed |
| Create a sandbox | Allowed, as a child of the scoped sandbox |
| Any call on another sandbox | 403 sandbox_out_of_scope |
| Templates, volumes, apps | 403 — 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 listcurl -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/auth/api-keysRevoke 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_123curl -X DELETE -H "Authorization: Bearer $PT_TOKEN" $PT_API_URL/v1/auth/api-keys/key_123Keys 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.