Plane docs
SDK and CLI

@plane/sdk/client

The API as promises. One key, one environment.

import { createPlaneClient, SessionNotFoundError } from "@plane/sdk/client"

const client = createPlaneClient({
  baseUrl: "https://plane.example.com",
  apiKey: process.env.PLANE_API_KEY,   // required; this is also the default
  headers: { "x-request-id": id },     // optional
  fetch: myFetch                       // optional
})

A key is required. createPlaneClient throws immediately without one, naming both ways to supply it, rather than letting every call come back Unauthorized from the far end of a round trip. An explicit headers.authorization wins and satisfies the check, for a caller already behind their own auth proxy.

There is no environment option and there will not be one. The key is the environment.

Nothing connects until the first call, so constructing a client at module scope in a serverless handler is free. close() releases the transport.

Surface

await client.me()                                  // who this key is, and where it points

await client.agents.list()
await client.agents.get({ id })
await client.agents.upsert({ slug, definition, expectedVersion })
await client.agents.versions({ id })
await client.agents.getVersion({ id, version })
await client.agents.imageStatus({ id })            // sandboxed agents; a read, never builds
await client.agents.rebake({ id })
await client.agents.promote({ agentId, to, mapping, dryRun })

await client.sessions.create({ agentId, initialMessage, metadata, identity, overrides })
await client.sessions.send({ id, message, mode, metadata, attachments, idempotencyKey })
await client.sessions.get({ id })
await client.sessions.list({ agentId, status, filter, limit, cursor })
await client.sessions.events({ id, after, limit })
await client.sessions.children({ id })
await client.sessions.annotate({ id, seq, metadata, mode })
await client.sessions.updateMetadata({ id, mode, metadata })
await client.sessions.pending({ id })              // approvals waiting
await client.sessions.decide({ id, toolCallId, decision, input, message })
await client.sessions.authorizations({ id })
await client.sessions.interrupt({ id })
await client.sessions.streamToken({ id, format, ttlSeconds, include })

await client.providers.upsert({ name, type, baseUrl, secret })
await client.providers.list()
await client.providers.models({ name })
await client.providers.test({ name })

await client.connections.upsert({ name, url, auth, hooksUrl, hookEvents })
await client.connections.test({ name })
await client.connections.jwks()
await client.connections.authorize({ connectionId, identity, redirectUri, sessionId, scopes })
await client.connections.grants.list({ identity })
await client.connections.grants.revoke({ id })
await client.connections.grants.import({ connectionId, mode, grants })

await client.secrets.upsert({ secret: { name, value, description } })
await client.secrets.list()
await client.secrets.delete({ name })

await client.hooks.stats({ since })
await client.hooks.deliveries({ status, connectionId, sessionId, since, cursor })
await client.hooks.redeliver({ id })
await client.hooks.redeliverAll({ status: "dead" })
await client.hooks.discard({ id })

await client.sandboxes.list({ sessionId })
await client.sandboxes.get({ id })
await client.sandboxes.snapshot({ id })
await client.sandboxes.resume({ id })
await client.sandboxes.release({ id })

await client.previews.mint({ sessionId, port, identity })
await client.previews.list({ sessionId })
await client.previews.revoke({ id })

await client.blobs.upload({ filename, contentType, data, sessionId })
await client.traces.forSession({ id, includeDescendants })
await client.usage.environment({ period, groupBy })
await client.usage.keys({ period })
await client.usage.session({ id })

await client.apiKeys.create({ key })
await client.apiKeys.list()
await client.apiKeys.update({ id, spend })
await client.apiKeys.revoke({ id })

await client.environments.list()
await client.environments.create({ slug, kind, group })
await client.environments.update({ slug, spend, retention })
await client.environments.archive({ slug })
await client.organizations.list()

await client.channels.issueToken({ kind: "dev", name: "jordan" })

await client.close()

The complete list, with every field and every error, is the generated reference.

Streaming, server-side

const controller = new AbortController()
for await (const event of client.sessions.stream(session.id, {
  after: 12,
  signal: controller.signal
})) {
  console.log(event.type)
}

For a browser, mint a token and use @plane/sdk/browser.

Results and errors

Results are plain JSON: timestamps are ISO strings, ids are plain strings, and absent optional fields are absent rather than undefined.

Write-only secrets — providers.upsert, a bearer connection, the tokens on grants.import — are passed as plain strings and wrapped for the wire internally. Omitting a provider's secret on an upsert keeps whatever Plane already stores, which is what makes a deploy script idempotent without holding the credential.

Typed failures throw PlaneError subclasses carrying the wire tag and the server's fields under data:

AgentNotFoundError, AgentNotReadyError, AgentNotPromotableError, SessionNotFoundError, EventNotFoundError, VersionConflictError, ConnectionNotFoundError, ConnectionNameTakenError, McpConnectionError, ProviderNotFoundError, ProviderNameTakenError, ProviderInUseError, ProviderRequestError, OverrideNotAllowedError, IdentityRequiredError, IdentityNotSwitchableError, IdentityScopeNotAllowedError, IdentityAttributesTooLargeError, GrantNotFoundError, RedirectUriNotAllowedError, OAuthFlowError, MetadataTooLargeError, SpendLimitExceededError, SpendCapExceedsParentError, PromotionUnresolvedError, DecisionNotPendingError, DecisionExpiredError.

A tag this version does not know still arrives as a PlaneError with that tag, so a contract addition does not break an old SDK. Transport and validation problems are PlaneTransportError and PlaneValidationError.

On this page