Skip to content

Authentication

Every v2 request must authenticate. There are two ways to do it, and which one you pick depends on whose data you are touching.

MethodUse it whenHeader
API keyA script, backend job, or integration acts as youX-Api-Key: <token>
OAuth 2.0An app acts on another user's behalf, with their consentAuthorization: Bearer <token>

Both are checked before anything else. A missing or invalid credential returns 401 unauthorized — see Errors.

Header casing

v2 prose and examples use X-Api-Key. The v1 docs write the same header as X-API-Key. HTTP header names are case-insensitive, so either spelling reaches the server, but match the v2 form when copying examples between versions.

API key

An API key is a personal access token. It authenticates as the user who created it.

Generate a key

  1. Log in to Plane and open Profile Settings.
  2. Go to the Personal Access Tokens tab.
  3. Click Add personal access token.
  4. Give it a title and description so you can tell later what it was for and where it is deployed.
  5. Optionally set an expiry so it stops working after a date you choose.

The token value is shown once. Store it somewhere your application can read it as a secret — never in client-side code or a committed file.

Use a key

bash
curl "https://api.plane.so/api/v2/users/me/" \
  -H "X-Api-Key: $PLANE_API_KEY"

API keys are not scope-limited

An API key acts with its owner's full permissions. It is not constrained by OAuth scopes — there is no way to mint a read-only key. It is still subject to the permission engine, so it can never do something the owner could not do in the UI, but within that boundary it can do everything.

Treat a key as equivalent to the account it belongs to. If one leaks, delete it in Personal Access Tokens and issue a new one.

OAuth 2.0

Use OAuth when a third-party app needs to act on behalf of a Plane user who is not you. v2 uses the standard authorization-code flow.

StepEndpoint
Authorization URL/auth/o/authorize/
Token URL/auth/o/token/
Request headerAuthorization: Bearer <access_token>
  1. Redirect the user to /auth/o/authorize/ with your client id, redirect URI, and the scopes you need.
  2. The user approves the scopes. Plane redirects back with an authorization code.
  3. Exchange that code for an access token at /auth/o/token/.
  4. Send the token on every API call.
bash
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/" \
  -H "Authorization: Bearer $PLANE_ACCESS_TOKEN"

OAuth tokens fail closed

Unlike an API key, an OAuth token is capped to the scopes the user granted. If the token lacks a scope that satisfies the operation, the request is rejected before the permission engine runs — a user with permission to do something does not help if your token was never granted the scope for it.

Request only the scopes you need. Adding one later requires the user to re-authorize.

The scope model

Scopes come in two tiers, and an operation is satisfied by either one (OR logic):

  • Global tierread grants read access to all resources; write grants write access to all resources.
  • Fine-grained<resource>:read and <resource>:write, for example projects.work_items:read or projects.states:write.

So GET …/states/ succeeds with either read or projects.states:read. You never need both, and a global tier scope is not a prerequisite for the fine-grained one.

Prefer fine-grained scopes

Ask for read or write only when your app genuinely spans the whole workspace surface. A consent screen listing three specific scopes converts better than one asking for everything, and it limits the blast radius if a token leaks.

Who am I

GET /api/v2/users/me/ echoes back whoever the credential you sent belongs to. It takes no workspace or project id, so it isolates an auth problem from a permission or tenancy problem — reach for it first when you are debugging a 401 or a 403.

GET/api/v2/users/me/

No fine-grained scope gates it. An API key can always call it; an OAuth token needs the global read scope — a token holding only fine-grained scopes such as projects.states:read cannot call it.

Who am I
bash
curl "https://api.plane.so/api/v2/users/me/" \
  -H "X-Api-Key: $PLANE_API_KEY"
Response200
json
{
  "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
  "email": "priya@my-team.io",
  "display_name": "priya",
  "principal_kind": "oauth",
  "scopes": ["projects.work_items:read", "projects.states:read"]
}

Response fields

  • id string (uuid)

    The authenticated user.

  • email string (email)

    The user's email address.

  • display_name string

    The user's display name.

  • principal_kind string

    How the request authenticated. One of oauth, api_key, or other.

  • scopes array of string

    The scopes this credential carries. Populated for OAuth tokens. Because API keys are not scope-limited, this tells you nothing about an API key's reach — check principal_kind first.

Debugging a 403

Call /api/v2/users/me/ and compare scopes against the scope listed on the endpoint page you are calling. If the scope is missing, the token needs re-authorization. If it is present and you still get 403, the block is the user's role, not the token.

Fine-grained scopes for live endpoints

These are the scopes that gate endpoints documented in this reference. Each is also satisfied by the matching global tier scope (read or write).

ScopeGrants
projects.work_items:readRead access to project work items
projects.work_items:writeWrite access to project work items
projects.work_items.comments:readRead access to work item comments
projects.work_items.comments:writeWrite access to work item comments
projects.states:readRead access to project states
projects.states:writeWrite access to project states
projects.labels:readRead access to project labels
projects.labels:writeWrite access to project labels
projects.cycles:readRead access to project cycles
projects.cycles:writeWrite access to project cycles
projects.modules:readRead access to project modules
projects.modules:writeWrite access to project modules
projects.work_item_types:readRead access to work item types
projects.work_item_types:writeWrite access to work item types
projects.work_item_properties:readRead access to work item properties
projects.work_item_properties:writeWrite access to work item properties
projects.members:readRead access to project members
workspaces.members:readRead access to workspace members
workspaces.work_item_types:readRead access to workspace work item types
workspaces.work_item_types:writeWrite access to workspace work item types
workspaces.work_item_properties:readRead access to workspace work item properties
workspaces.work_item_properties:writeWrite access to workspace work item properties
workspaces.features:readRead access to workspace features
workspaces.features:writeWrite access to workspace features
workspaces.audit_logs:readRead access to workspace audit logs
readRead access to all resources
writeWrite access to all resources

The catalog is larger than this table

The OAuth scope catalog advertises the full planned resource surface — pages, customers, initiatives, releases, teamspaces, templates, webhooks, assets, and more. Those scopes exist and can be requested, but their endpoints are not live in v2 yet, so only the scopes above currently gate real requests.

The complete catalog is served under components.securitySchemes.oauth2 in GET /api/v2/schema/.

The scope required by a specific operation is listed in the Scopes section of each endpoint page — for example Create a state requires projects.states:write.

Tenancy

Authentication tells Plane who you are; tenancy decides what you can see. A workspace slug, project id, or record id outside your tenant returns 404 resource_not_found, never 403.

A cross-tenant id looks identical to a deleted one

This is deliberate — the API never confirms that a resource you cannot access exists. When a request that should work returns 404, verify the workspace and project the id belongs to before assuming the record is gone.

Reserve 403 forbidden in your error handling for the case it actually means: you can see the resource, but your role or token scope cannot perform this action.

Next

  • Errors — how 401, 403, and 404 are reported.
  • Introduction — base URL, trailing slashes, and request conventions.