Plane docs
Concepts

Environments, keys and scopes

The isolation unit, the credential that names it, and what a credential may do.

Organization → Environment. Two levels, and no third.

An organization is your company: users, memberships, and one set of signing keys. An environment is the isolation unit and the only scope any row carries. Agents, sessions, connections, providers, secrets, API keys, usage and spend caps all belong to exactly one environment.

await client.environments.create({
  slug: "prod",
  kind: "production",          // "development" | "production" | "personal"
  group: "customer-facing"
})
await client.environments.list()
await client.environments.archive({ slug: "old-staging" })   // keeps rows, refuses writes

Environments are free and encouraged — dev and prod at a small company, one per developer at a large one — and an environment slug is unique per organization, so two organizations both having a prod is the ordinary case.

An id belonging to another environment answers 404, never 403. The row was not seen, so the API is not an existence oracle for a tenant you cannot reach.

API keys

A key is a credential and an environment. There is no environment option on the client and there will not be one: PLANE_API_KEY=pl_prod_… plane sync writes to prod. The slug inside the token is a hint for a human reading a .env file — Plane hashes the whole string — but it is the hint that catches the mistake a silent success would ship.

const { key, plaintext } = await client.apiKeys.create({
  key: {
    name: "ci",
    scopes: ["read", "control"],
    identityScopes: ["system:*"],       // what identities this key may assert
    expiresAt: "2027-01-01T00:00:00Z",
    spend: { costUsdPerMonth: 200 }
  }
})
// `plaintext` is returned once and never again.

await client.apiKeys.list()
await client.apiKeys.revoke({ id })
await client.me()      // who this key is, and which environment it targets

Scopes

read, data, control — with no implication between them. A control key cannot list agents unless it also holds read.

ScopeRoughlyHeld by
readLists, gets, events, traces, usage.A dashboard, a metrics job.
datasessions.create, sessions.send, sessions.decide, sessions.streamToken, blobs.upload, previews.mint.Your application server.
controlAgents, providers, connections, secrets, environments, keys.A deploy pipeline.

Every operation in the reference states what it costs, and the document carries it as x-plane-scope, so "what does this key need" is answerable from the contract alone. auth.me costs any — it is how a client discovers what it holds.

A key without a scope is refused with Forbidden, carrying what it needed and what it held. A missing, revoked or expired one is Unauthorized, carrying the reason.

Roles, for people

A person signing in through the control plane is resolved against your identity provider's memberships on every request, not at sign-in — so revoking somebody takes effect on their next call rather than in twelve hours. A role collapses onto the same three scopes:

RoleScopes
owner, adminread, data, control
memberread, data, control — except no control inside a production environment
viewerread

An unrecognised role reads as viewer.

Spend caps

Caps are set on an environment and on individual keys, and are checked at sessions.create, sessions.send and when an agent spawns a sub-agent — never mid-turn.

await client.environments.update({
  slug: "dev",
  spend: { costUsdPerMonth: 50, tokensPerMonth: 20_000_000 }
})

A running turn always finishes, and answering an approval (sessions.decide) is never gated: refusing one would strand a session in requires_action forever, which is worse than one turn of overage. Exceeding a cap is SpendLimitExceeded — a 402, not a 429, because a monthly cap is not a rate and retrying it with backoff is exactly wrong. See usage and spend.

Retention

Each environment carries a retention policy with five windows in days. An omitted field means unlimited, never "off". See retention.

On this page