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. Five resources are scopeable, each matching its /v1/<resource> route group:
| Resource | Covers | Actions |
|---|---|---|
sandboxes | Create and drive sandboxes — sandboxes | read, write |
templates | Build and version templates — templates | read, write |
volumes | Persistent disks to mount — volumes | read, write |
apps | Long-running deployments | read, write |
secrets | Organization secrets the egress proxy injects — secrets | read, write, use |
| Scope token | Grants |
|---|---|
* | Everything (the default) |
<resource>:read | Read-only (GET/HEAD) |
<resource>:write | Write, implies read |
secrets:use | Bind a secret to a sandbox |
<resource> and <resource>:* grant every action the resource lists. For secrets that includes use. Account-level routes — auth, billing, org, audit, key management — require *. The one exception is GET /v1/auth/me.
A token naming an action its resource does not list is rejected at mint. sandboxes:use is an error.
List the full scope model with pt key scopes.
The secrets scopes
secrets:write does not grant secrets:use. The two are separate capabilities:
| Scope | Holder can |
|---|---|
secrets:read | List secrets and their metadata. Values are never returned. |
secrets:write | Create, rotate, and delete secrets. |
secrets:use | Bind a secret to a sandbox, so the proxy writes its value onto that sandbox's outbound requests. |
Warning: anyone who can edit a secret's allowed hosts can read its value. They point the allowlist at a host they control, then drive the sandbox to make one request. Editing the allowed hosts or header names therefore requires secrets:use in addition to secrets:write.
Neither the dashboard "Read-only" preset nor any one-click preset includes a secrets scope. Assemble secrets scopes under Custom.
Organization secrets are not grantable over OAuth. Use an API key you mint yourself.
Service integrations
A userless service key cannot manage organization secrets by default, including
a legacy key with *. An operator must add the exact key id to
PT_SECRET_MANAGER_API_KEY_IDS on the control plane. The key must also grant
both secrets:write and secrets:use.
This two-part gate lets a server create, rotate, bind, and revoke protected credentials without granting that authority to every legacy full-access key. Key rotation requires adding the replacement id before removing the old id.
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, secrets | 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.