Plane docs
API reference

Errors

Branch on the tag. The status is a pure function of it.

A failure is always a JSON body with a _tag and the error's own fields — exactly the shape the contract declares.

{
  "_tag": "Forbidden",
  "required": "control",
  "held": ["read", "data"]
}

Branch on the tag. The status is a summary of something you can already read, and it is a pure function of the tag, so the same error means the same status wherever it is raised.

StatusMeaningExamples
400The request is not acceptable input. A body that does not decode lands here as BadRequest.MetadataFilterInvalid, MetadataTooLarge, BlobTooLarge, TtlTooLong, RetentionInvalid
401No usable credential.Unauthorized, carrying a reason: unknown, revoked, expired
402The spend cap.SpendLimitExceeded
403A credential that is fine, refused for what it asked.Forbidden, carrying required and held
404It is not here.every *NotFound
409It is already there, or it moved.ProviderNameTaken, ConnectionNameTaken, EnvironmentSlugTaken, VersionConflict
422Understood, and refused on its merits.OverrideNotAllowed, ProviderInUse, SecretInUse, AgentNotReady, PromotionUnresolved, DecisionExpired, ConnectionError, ProviderError

Three that surprise people

402, not 429, for the spend cap. 429 means "slow down", and every client library retries it with backoff; a monthly cap is not a rate and retrying it is exactly wrong. SpendLimitExceeded carries scope, metric, window and resetsAt, so a caller that wants to wait knows how long. Nothing should retry it automatically.

A cross-environment id is a 404, never a 403. The row was not seen, so the API is not an existence oracle for another tenant's ids.

422 for OverrideNotAllowed, rather than ignoring the field. A caller who asked for a cheaper model and silently got the expensive one has no way to find out.

In the SDK

Typed failures arrive as PlaneError subclasses, each carrying the wire tag and the server's fields under data.

import { SpendLimitExceededError, DecisionExpiredError } from "@plane/sdk/client"

try {
  await client.sessions.send({ id, message })
} catch (error) {
  if (error instanceof SpendLimitExceededError) {
    console.warn(`${error.scope} out of ${error.metric} until ${error.resetsAt}`)
    return
  }
  throw error
}

A tag this version of the SDK does not know still arrives as a PlaneError with that tag, so a contract addition never breaks an older client. Transport and validation problems are PlaneTransportError and PlaneValidationError.

On this page