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.
| Method | Use it when | Header |
|---|---|---|
| API key | A script, backend job, or integration acts as you | X-Api-Key: <token> |
| OAuth 2.0 | An app acts on another user's behalf, with their consent | Authorization: Bearer <token> |
| OAuth Bridge | An internal app sends tokens from your own identity provider | Authorization: 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
- Log in to Plane and open Profile Settings.
- Go to the Personal Access Tokens tab.
- Click Add personal access token.
- Give it a title and description so you can tell later what it was for and where it is deployed.
- 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
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.
| Step | Endpoint |
|---|---|
| Authorization URL | /auth/o/authorize/ |
| Token URL | /auth/o/token/ |
| Request header | Authorization: Bearer <access_token> |
- Redirect the user to
/auth/o/authorize/with your client id, redirect URI, and the scopes you need. - The user approves the scopes. Plane redirects back with an authorization code.
- Exchange that code for an access token at
/auth/o/token/. - Send the token on every API call.
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.
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.
curl "https://api.plane.so/api/v2/users/me/" \
-H "X-Api-Key: $PLANE_API_KEY"import requests
response = requests.get(
"https://api.plane.so/api/v2/users/me/",
headers={"X-Api-Key": "your-api-key"},
)
print(response.json())const response = await fetch("https://api.plane.so/api/v2/users/me/", {
headers: { "X-Api-Key": "your-api-key" },
});
const data = await response.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
idstring (uuid)The authenticated user.
emailstring (email)The user's email address.
display_namestringThe user's display name.
principal_kindstringHow the request authenticated. One of
oauth,api_key, orother.scopesarray of stringThe 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_kindfirst.
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.
| Scope | Grants |
|---|---|
assets:read | Read access to assets |
assets:write | Write access to assets |
automations:read | Read access to automations |
automations:write | Write access to automations |
customers.properties:read | Read access to customer properties |
customers.properties:write | Write access to customer properties |
customers.property_values:read | Read access to customer property values |
customers.property_values:write | Write access to customer property values |
customers.requests:read | Read access to customer requests |
customers.requests:write | Write access to customer requests |
customers:read | Read access to customers |
customers:write | Write access to customers |
git.meta:write | Write branches, commits and pull requests against work items |
initiatives.labels:read | Read access to initiatives labels |
initiatives.labels:write | Write access to initiatives labels |
initiatives:read | Read access to initiatives |
initiatives:write | Write access to initiatives |
notifications:read | Read access to user notifications |
projects.cycles:read | Read access to project cycles |
projects.cycles:write | Write access to project cycles |
projects.estimates:read | Read access to project estimates |
projects.estimates:write | Write access to project estimates |
projects.features:read | Read access to project features |
projects.features:write | Write access to project features |
projects.intakes:read | Read access to project intakes |
projects.intakes:write | Write access to project intakes |
projects.labels:read | Read access to project labels |
projects.labels:write | Write access to project labels |
projects.members:read | Read access to project members |
projects.members:write | Write access to project members |
projects.milestones:read | Read access to project milestones |
projects.milestones:write | Write access to project milestones |
projects.modules:read | Read access to project modules |
projects.modules:write | Write access to project modules |
projects.pages:read | Read access to project pages |
projects.pages:write | Write access to project pages |
projects.recurring_work_items:read | Read access to recurring work items |
projects.recurring_work_items:write | Write access to recurring work items |
projects.states:read | Read access to project states |
projects.states:write | Write access to project states |
projects.views:read | Read access to project views |
projects.views:write | Write access to project views |
projects.work_item_properties:read | Read access to work item properties |
projects.work_item_properties:write | Write access to work item properties |
projects.work_item_types:read | Read access to work item types |
projects.work_item_types:write | Write access to work item types |
projects.work_items.activities:read | Read access to work item activities |
projects.work_items.attachments:read | Read access to work item attachments |
projects.work_items.attachments:write | Write access to work item attachments |
projects.work_items.comments:read | Read access to work item comments |
projects.work_items.comments:write | Write access to work item comments |
projects.work_items.links:read | Read access to work item links |
projects.work_items.links:write | Write access to work item links |
projects.work_items.relations:read | Read access to work item relations |
projects.work_items.relations:write | Write access to work item relations |
projects.work_items.worklogs:read | Read access to work item worklogs |
projects.work_items.worklogs:write | Write access to work item worklogs |
projects.work_items:read | Read access to project work items |
projects.work_items:write | Write access to project work items |
projects.workflows:read | Read access to project workflows |
projects.workflows:write | Write access to project workflows |
projects:read | Read access to projects |
projects:write | Write access to projects |
releases.comments:read | Read access to release comments |
releases.comments:write | Write access to release comments |
releases.labels:read | Read access to release labels |
releases.labels:write | Write access to release labels |
releases.links:read | Read access to release links |
releases.links:write | Write access to release links |
releases.tags:read | Read access to release tags |
releases.tags:write | Write access to release tags |
releases:read | Read access to releases |
releases:write | Write access to releases |
stickies:read | Read access to stickies |
stickies:write | Write access to stickies |
teamspaces:read | Read access to teamspaces |
teamspaces:write | Write access to teamspaces |
template_categories:read | Read access to template categories |
templates.pages:read | Read access to page templates |
templates.pages:write | Write access to page templates |
templates.projects:read | Read access to project templates |
templates.projects:write | Write access to project templates |
templates.work_items:read | Read access to work item templates |
templates.work_items:write | Write access to work item templates |
webhooks:read | Read access to workspace webhooks |
webhooks:write | Write access to workspace webhooks |
wiki.pages:read | Read access to wiki pages |
wiki.pages:write | Write access to wiki pages |
workspaces.artifacts:read | Read access to workspace artifacts |
workspaces.artifacts:write | Write access to workspace artifacts |
workspaces.audit_logs:read | Read access to workspace audit logs |
workspaces.features:read | Read access to workspace features |
workspaces.features:write | Write access to workspace features |
workspaces.group_sync:read | Read access to workspace IdP group sync config and mappings |
workspaces.group_sync:write | Write access to workspace IdP group sync config and mappings |
workspaces.members:read | Read access to workspace members |
workspaces.members:write | Write access to workspace members |
workspaces.permissions:read | Read access to the calling user's effective permissions |
workspaces.permissions:write | Write access to resource permission grants |
workspaces.project_labels:read | Read access to workspace project labels |
workspaces.project_labels:write | Write access to workspace project labels |
workspaces.project_states:read | Read access to workspace project states |
workspaces.project_states:write | Write access to workspace project states |
workspaces.roles:read | Read access to workspace roles (system + custom) |
workspaces.states:read | Read access to workspace states |
workspaces.states:write | Write access to workspace (catalog) states |
workspaces.views:read | Read access to workspace views |
workspaces.views:write | Write access to workspace views |
workspaces.work_item_properties:read | Read access to workspace work item properties |
workspaces.work_item_properties:write | Write access to workspace work item properties |
workspaces.work_item_relation_definitions:read | Read access to workspace work item relation definitions |
workspaces.work_item_relation_definitions:write | Write access to workspace work item relation definitions |
workspaces.work_item_types:read | Read access to workspace work item types |
workspaces.work_item_types:write | Write access to workspace work item types |
workspaces.workflows:read | Read access to workspace workflows |
workspaces.workflows:write | Write access to workspace (catalog) workflows |
workspaces:read | Read access to workspaces |
read | Read access to all resources |
write | Write 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, and404are reported. - Introduction: base URL, trailing slashes, and request conventions.

