Skip to content

Authentication ​

Every v2 request must authenticate. There are three methods. Which one to use depends on whose data you are accessing and where your tokens come from.

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>
OAuth BridgeAn internal app sends tokens from your own identity providerAuthorization: Bearer <token>

Plane checks the credential 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 as a secret that your application can read. Never put it 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. OAuth scopes do not apply to it, so you cannot create a read-only key. The permission engine still applies: a key can never do something its owner could not do in the UI, but it can do everything the owner can.

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 limited to the scopes the user granted. If the token lacks a scope that satisfies the operation, Plane rejects the request before the permission engine runs. The user's own permissions do not matter if the token was never granted the scope.

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

OAuth Bridge ​

If your apps already get access tokens from your own identity provider, such as Microsoft Entra ID, ADFS, or Okta, a workspace admin can install OAuth Bridge so Plane accepts those tokens directly. Send the IdP's token as Authorization: Bearer <token>. Plane checks it against the provider's public keys, matches the user by email, and runs the request as that user.

Like an API key, an OAuth Bridge token acts with the user's full permissions and isn't limited by OAuth scopes. See OAuth Bridge for setup, user matching, and errors.

The scope model ​

Scopes come in two tiers, and a scope from either tier satisfies an operation. The global tier has read, which grants read access to all resources, and write, which grants write access to all resources. The fine-grained tier has <resource>:read and <resource>:write scopes, for example projects.work_items:read or projects.states:write.

For example, GET …/states/ succeeds with either read or projects.states:read. You never need both, and a fine-grained scope does not require the matching global scope.

Prefer fine-grained scopes

Ask for read or write only when your app needs every resource in the workspace. Users approve a consent screen that lists three specific scopes more readily than one that asks for everything, and narrow scopes limit the damage if a token leaks.

Who am I ​

GET /api/v2/users/me/ returns the user that the credential belongs to. It takes no workspace or project id, so it separates an authentication problem from a permission or tenancy problem. Call it first when you debug a 401 or a 403.

GET/api/v2/users/me/

No fine-grained scope covers this endpoint. An API key can always call it. An OAuth token needs the global read scope; a token with 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. API keys are not scope-limited, so for an API key this field says nothing about what the key can access. 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 user's role is blocking the request, not the token.

Fine-grained scopes for live endpoints ​

v2 endpoints check these scopes. The matching global tier scope (read or write) also satisfies each one, with three exceptions. The two effective-permission reads (GET …/permissions/me/ at the workspace and project level) accept only workspaces.permissions:read, and the project worklog summary accepts only projects.work_items.worklogs:read.

ScopeGrants
assets:readRead access to assets
assets:writeWrite access to assets
automations:readRead access to automations
automations:writeWrite access to automations
customers.properties:readRead access to customer properties
customers.properties:writeWrite access to customer properties
customers.property_values:readRead access to customer property values
customers.property_values:writeWrite access to customer property values
customers.requests:readRead access to customer requests
customers.requests:writeWrite access to customer requests
customers:readRead access to customers
customers:writeWrite access to customers
git.meta:writeWrite branches, commits and pull requests against work items
initiatives.labels:readRead access to initiatives labels
initiatives.labels:writeWrite access to initiatives labels
initiatives:readRead access to initiatives
initiatives:writeWrite access to initiatives
notifications:readRead access to user notifications
projects.cycles:readRead access to project cycles
projects.cycles:writeWrite access to project cycles
projects.estimates:readRead access to project estimates
projects.estimates:writeWrite access to project estimates
projects.features:readRead access to project features
projects.features:writeWrite access to project features
projects.intakes:readRead access to project intakes
projects.intakes:writeWrite access to project intakes
projects.labels:readRead access to project labels
projects.labels:writeWrite access to project labels
projects.members:readRead access to project members
projects.members:writeWrite access to project members
projects.milestones:readRead access to project milestones
projects.milestones:writeWrite access to project milestones
projects.modules:readRead access to project modules
projects.modules:writeWrite access to project modules
projects.pages:readRead access to project pages
projects.pages:writeWrite access to project pages
projects.recurring_work_items:readRead access to recurring work items
projects.recurring_work_items:writeWrite access to recurring work items
projects.states:readRead access to project states
projects.states:writeWrite access to project states
projects.views:readRead access to project views
projects.views:writeWrite access to project views
projects.work_item_properties:readRead access to work item properties
projects.work_item_properties:writeWrite access to work item properties
projects.work_item_types:readRead access to work item types
projects.work_item_types:writeWrite access to work item types
projects.work_items.activities:readRead access to work item activities
projects.work_items.attachments:readRead access to work item attachments
projects.work_items.attachments:writeWrite access to work item attachments
projects.work_items.comments:readRead access to work item comments
projects.work_items.comments:writeWrite access to work item comments
projects.work_items.links:readRead access to work item links
projects.work_items.links:writeWrite access to work item links
projects.work_items.relations:readRead access to work item relations
projects.work_items.relations:writeWrite access to work item relations
projects.work_items.worklogs:readRead access to work item worklogs
projects.work_items.worklogs:writeWrite access to work item worklogs
projects.work_items:readRead access to project work items
projects.work_items:writeWrite access to project work items
projects.workflows:readRead access to project workflows
projects.workflows:writeWrite access to project workflows
projects:readRead access to projects
projects:writeWrite access to projects
releases.comments:readRead access to release comments
releases.comments:writeWrite access to release comments
releases.labels:readRead access to release labels
releases.labels:writeWrite access to release labels
releases.links:readRead access to release links
releases.links:writeWrite access to release links
releases.tags:readRead access to release tags
releases.tags:writeWrite access to release tags
releases:readRead access to releases
releases:writeWrite access to releases
stickies:readRead access to stickies
stickies:writeWrite access to stickies
teamspaces:readRead access to teamspaces
teamspaces:writeWrite access to teamspaces
template_categories:readRead access to template categories
templates.pages:readRead access to page templates
templates.pages:writeWrite access to page templates
templates.projects:readRead access to project templates
templates.projects:writeWrite access to project templates
templates.work_items:readRead access to work item templates
templates.work_items:writeWrite access to work item templates
webhooks:readRead access to workspace webhooks
webhooks:writeWrite access to workspace webhooks
wiki.pages:readRead access to wiki pages
wiki.pages:writeWrite access to wiki pages
workspaces.artifacts:readRead access to workspace artifacts
workspaces.artifacts:writeWrite access to workspace artifacts
workspaces.audit_logs:readRead access to workspace audit logs
workspaces.features:readRead access to workspace features
workspaces.features:writeWrite access to workspace features
workspaces.group_sync:readRead access to workspace IdP group sync config and mappings
workspaces.group_sync:writeWrite access to workspace IdP group sync config and mappings
workspaces.members:readRead access to workspace members
workspaces.members:writeWrite access to workspace members
workspaces.permissions:readRead access to the calling user's effective permissions
workspaces.permissions:writeWrite access to resource permission grants
workspaces.project_labels:readRead access to workspace project labels
workspaces.project_labels:writeWrite access to workspace project labels
workspaces.project_states:readRead access to workspace project states
workspaces.project_states:writeWrite access to workspace project states
workspaces.roles:readRead access to workspace roles (system + custom)
workspaces.states:readRead access to workspace states
workspaces.states:writeWrite access to workspace (catalog) states
workspaces.views:readRead access to workspace views
workspaces.views:writeWrite access to workspace views
workspaces.work_item_properties:readRead access to workspace work item properties
workspaces.work_item_properties:writeWrite access to workspace work item properties
workspaces.work_item_relation_definitions:readRead access to workspace work item relation definitions
workspaces.work_item_relation_definitions:writeWrite access to workspace work item relation definitions
workspaces.work_item_types:readRead access to workspace work item types
workspaces.work_item_types:writeWrite access to workspace work item types
workspaces.workflows:readRead access to workspace workflows
workspaces.workflows:writeWrite access to workspace (catalog) workflows
workspaces:readRead access to workspaces
readRead access to all resources
writeWrite access to all resources

The OAuth catalog is larger than this table

The OAuth scope catalog also holds scopes that no v2 endpoint checks yet, for example agents.runs:read, scripts:read, and git.repositories:read. Your app can request them, but they don't open any v2 route.

The v2 schema at GET /api/v2/schema/ lists only the scopes its operations use, under components.securitySchemes.oauth2. That list matches this table.

Each endpoint page lists the scope it requires in its Scopes section. 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 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.

In your error handling, treat 403 forbidden as meaning one thing: you can see the resource, but your role or token scope does not allow this action.

Next ​

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