# Scopes and permissions

Every operation needs a token scope and a permission on the user's role. How both are built, what each covers and which one to fix when a call is refused.

Every `/api/v2` request passes two separate checks, and every operation page states what each one needs.

| Check           | What it is                                                 | Who controls it                                                | When it fails                                                                                                                                                                                  |
| --------------- | ---------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Token scope** | An `api:` ability on the token you sent.                   | Whoever created the token, or the person who approved the app. | `403` [insufficient-token-scope](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#insufficient-token-scope), with `required_ability` listing the abilities that would have worked. |
| **Permission**  | A permission on the role of the user the token belongs to. | Whoever manages roles.                                         | `403` [insufficient-permission](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#insufficient-permission), with `required_permission` naming the missing one.                      |

The token is only ever a ceiling. Permissions are read again on every request, so taking a permission away from a role narrows existing tokens at once, without touching them.

The two failures have different fixes. A scope failure needs a different token; a permission failure needs a role change. The full order of checks, from the organisation's switch to the record's state, is in [Authentication](https://developers.plnnrly.com/en/docs/api/v2/guides/authentication/#the-order-of-checks).

## How a scope is built

An ability is `api:{class}` or `api:{class}:{prefix}`.

- **class** is `read`, `write` or `destroy`. On the **API tokens** page they are called **Read**, **Write** and **Delete**.
- **prefix** is the resource, spelled as in the permission names, such as `projects`, `project_management` or `work_types`. [API tokens](https://developers.plnnrly.com/en/docs/api/v2/guides/tokens/#resources) maps each prefix to the name the token form shows.

Leave out the prefix and the ability covers every resource of its class: `api:write` satisfies an operation that asks for `api:write:projects`.

An app that [signs people in with OAuth](https://developers.plnnrly.com/en/docs/api/v2/guides/oauth-apps/) asks for these same strings as OAuth scopes, except the destroy class, which OAuth never offers.

## The class ladder

Each class includes the ones below it: `write` includes `read`, and `destroy` includes both.

| Request         | Class required | Satisfied by               |
| --------------- | -------------- | -------------------------- |
| `GET`           | `read`         | `read`, `write`, `destroy` |
| `POST`, `PATCH` | `write`        | `write`, `destroy`         |
| `DELETE`        | `destroy`      | `destroy`                  |

Every `DELETE` needs the destroy class, whether it deletes softly or for good. `force=true` is accepted on [DELETE /projects/{project}](https://developers.plnnrly.com/en/docs/api/v2/projects/delete/) alone. There it deletes the project for good, and it checks the same `projects::delete` permission as a soft delete. See [Deleting and restoring](https://developers.plnnrly.com/en/docs/api/v2/guides/deleting-and-restoring/).

Some operations list more than one scope. Any one of them is enough. They are the drive operations, where one endpoint serves items owned by a project, a customer, the organisation or a user, plus the comment and upload operations that work items and drive items share.

## Token scopes

A token can carry every class with every resource, plus the three unnarrowed abilities `api:read`, `api:write` and `api:destroy`. 82 of them are named by at least one operation, and the table below lists those, with what each covers. `api:write` and `api:destroy` are not in it because no operation names them, but `api:write` satisfies every read and write, and `api:destroy` every read, write and delete.

| Scope | Resources | Operations |
| --- | --- | --- |
| `api:read` | `/`, `/me` | 2 |
| `api:read:absences` | `/absences` | 2 |
| `api:read:budget_groups` | `/budget-groups` | 2 |
| `api:read:budgets` | `/budgets` | 2 |
| `api:read:checkins` | `/checkins` | 2 |
| `api:read:company_branches` | `/company-branches` | 2 |
| `api:read:customer_categories` | `/customer-categories` | 2 |
| `api:read:customer_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 12 |
| `api:read:customer_subscriptions` | `/customer-subscriptions`, `/subscription-items` | 4 |
| `api:read:customers` | `/contact-roles`, `/contacts`, `/customers` | 6 |
| `api:read:events` | `/events` | 2 |
| `api:read:free_days` | `/free-days` | 2 |
| `api:read:furlough` | `/furlough-balance-mutations` | 2 |
| `api:read:holidays` | `/holidays` | 2 |
| `api:read:hours` | `/timers` | 2 |
| `api:read:invoices` | `/invoices` | 2 |
| `api:read:kanban` | `/projects`, `/tasks` | 2 |
| `api:read:organization_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 12 |
| `api:read:project_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 12 |
| `api:read:project_management` | `/comments`, `/labels`, `/sprints`, `/subtasks`, `/todos`, `/uploads`, `/work-item-links`, `/work-items` | 16 |
| `api:read:projects` | `/project-links`, `/projects` | 4 |
| `api:read:sales_invoices` | `/sales-invoice-rows`, `/sales-invoices` | 4 |
| `api:read:subscription_products` | `/subscription-products` | 2 |
| `api:read:subscription_types` | `/subscription-type-items`, `/subscription-types` | 4 |
| `api:read:times` | `/worklogs` | 2 |
| `api:read:user_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 12 |
| `api:read:users` | `/users` | 2 |
| `api:read:work_types` | `/work-types` | 2 |
| `api:read:workflows` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 6 |
| `api:write:absences` | `/absences` | 2 |
| `api:write:budget_groups` | `/budget-groups` | 2 |
| `api:write:budgets` | `/budgets` | 2 |
| `api:write:checkins` | `/checkins` | 2 |
| `api:write:company_branches` | `/company-branches` | 2 |
| `api:write:customer_categories` | `/customer-categories` | 2 |
| `api:write:customer_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 13 |
| `api:write:customer_subscriptions` | `/customer-subscriptions`, `/subscription-items` | 5 |
| `api:write:customers` | `/contact-roles`, `/contacts`, `/customers` | 7 |
| `api:write:events` | `/events` | 3 |
| `api:write:free_days` | `/free-days` | 2 |
| `api:write:furlough` | `/furlough-balance-mutations` | 2 |
| `api:write:holidays` | `/holidays` | 2 |
| `api:write:hours` | `/timers` | 2 |
| `api:write:invoices` | `/invoices` | 1 |
| `api:write:kanban` | `/projects`, `/tasks` | 2 |
| `api:write:organization_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 13 |
| `api:write:project_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 13 |
| `api:write:project_management` | `/comments`, `/labels`, `/sprints`, `/subtasks`, `/todos`, `/work-items` | 19 |
| `api:write:projects` | `/project-links`, `/projects` | 5 |
| `api:write:sales_invoices` | `/sales-invoice-rows`, `/sales-invoices` | 5 |
| `api:write:subscription_products` | `/subscription-products` | 3 |
| `api:write:subscription_types` | `/subscription-type-items`, `/subscription-types` | 5 |
| `api:write:times` | `/worklogs` | 2 |
| `api:write:user_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 13 |
| `api:write:work_types` | `/work-types` | 2 |
| `api:write:workflows` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 6 |
| `api:destroy:absences` | `/absences` | 1 |
| `api:destroy:budget_groups` | `/budget-groups` | 1 |
| `api:destroy:budgets` | `/budgets` | 1 |
| `api:destroy:checkins` | `/checkins` | 1 |
| `api:destroy:company_branches` | `/company-branches` | 1 |
| `api:destroy:customer_categories` | `/customer-categories` | 1 |
| `api:destroy:customer_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 6 |
| `api:destroy:customer_subscriptions` | `/customer-subscriptions`, `/subscription-items` | 2 |
| `api:destroy:customers` | `/contact-roles`, `/contacts`, `/customers` | 3 |
| `api:destroy:events` | `/events` | 1 |
| `api:destroy:free_days` | `/free-days` | 1 |
| `api:destroy:furlough` | `/furlough-balance-mutations` | 1 |
| `api:destroy:holidays` | `/holidays` | 1 |
| `api:destroy:hours` | `/timers` | 1 |
| `api:destroy:kanban` | `/tasks` | 1 |
| `api:destroy:organization_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 6 |
| `api:destroy:project_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 6 |
| `api:destroy:project_management` | `/comments`, `/labels`, `/sprints`, `/subtasks`, `/todos`, `/uploads`, `/work-item-links`, `/work-items` | 8 |
| `api:destroy:projects` | `/project-links`, `/projects` | 2 |
| `api:destroy:sales_invoices` | `/sales-invoice-rows`, `/sales-invoices` | 2 |
| `api:destroy:subscription_products` | `/subscription-products` | 1 |
| `api:destroy:subscription_types` | `/subscription-type-items`, `/subscription-types` | 2 |
| `api:destroy:times` | `/worklogs` | 1 |
| `api:destroy:user_drive` | `/comments`, `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes`, `/uploads` | 6 |
| `api:destroy:work_types` | `/work-types` | 1 |
| `api:destroy:workflows` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 3 |

OAuth apps may ask for the read and write abilities only.

## Permissions

67 permissions gate `/api/v2`. They are the same permissions plnnrly checks in the app, so an integration can do exactly what the user behind the token can do there, and no more.

A permission is `{prefix}::{action}`, such as `projects::update`. The prefix is usually the one a narrowed scope names, so `api:write:projects` and `projects::update` guard the same thing from two sides. Time is the exception: reading worklogs needs the scope `api:read:times` and the permission `hours::read`.

| Permission | Resources | Operations |
| --- | --- | --- |
| `absences::manage` | `/absences` | 5 |
| `budget_groups::create` | `/budget-groups` | 1 |
| `budget_groups::delete` | `/budget-groups` | 1 |
| `budget_groups::read` | `/budget-groups` | 2 |
| `budget_groups::update` | `/budget-groups` | 1 |
| `budgets::create` | `/budgets` | 1 |
| `budgets::delete` | `/budgets` | 1 |
| `budgets::read` | `/budgets` | 2 |
| `budgets::update` | `/budgets` | 1 |
| `checkins::read` | `/checkins` | 2 |
| `company_branches::create` | `/company-branches` | 1 |
| `company_branches::delete` | `/company-branches` | 1 |
| `company_branches::read` | `/company-branches` | 2 |
| `company_branches::update` | `/company-branches` | 1 |
| `customer_categories::create` | `/customer-categories` | 1 |
| `customer_categories::delete` | `/customer-categories` | 1 |
| `customer_categories::read` | `/customer-categories` | 2 |
| `customer_categories::update` | `/customer-categories` | 1 |
| `customer_subscriptions::create` | `/customer-subscriptions` | 3 |
| `customer_subscriptions::delete` | `/customer-subscriptions`, `/subscription-items` | 2 |
| `customer_subscriptions::read` | `/customer-subscriptions`, `/subscription-items` | 4 |
| `customer_subscriptions::update` | `/customer-subscriptions`, `/subscription-items` | 2 |
| `customers::create` | `/contact-roles`, `/contacts`, `/customers` | 4 |
| `customers::delete` | `/contact-roles`, `/contacts`, `/customers` | 3 |
| `customers::read` | `/contact-roles`, `/contacts`, `/customers` | 6 |
| `customers::update` | `/contact-roles`, `/contacts`, `/customers` | 3 |
| `events::manage_self` | `/events` | 6 |
| `free_days::manage` | `/free-days` | 5 |
| `furlough::create_for_yourself` | `/furlough-balance-mutations` | 1 |
| `furlough::manage` | `/furlough-balance-mutations` | 2 |
| `holidays::manage` | `/holidays` | 5 |
| `hours::read` | `/timers`, `/worklogs` | 4 |
| `invoices::create` | `/invoices` | 1 |
| `invoices::read` | `/invoices` | 2 |
| `kanban::manage_self` | `/projects`, `/tasks` | 5 |
| `project_drive::access` | `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 10 |
| `project_drive::create` | `/drive-documents`, `/drive-folders`, `/drive-items`, `/notes` | 16 |
| `project_management::manage_sprints` | `/sprints` | 5 |
| `project_management::manage_work_items` | `/comments`, `/labels`, `/subtasks`, `/todos`, `/work-item-links`, `/work-items` | 21 |
| `project_management::view_active_sprint` | `/sprints` | 2 |
| `project_management::view_backlog` | `/comments`, `/labels`, `/subtasks`, `/todos`, `/work-item-links`, `/work-items` | 13 |
| `projects::create` | `/projects` | 2 |
| `projects::delete` | `/projects` | 1 |
| `projects::links` | `/project-links` | 5 |
| `projects::read` | `/projects` | 2 |
| `projects::update` | `/projects` | 1 |
| `sales_invoices::create` | `/sales-invoices` | 3 |
| `sales_invoices::delete` | `/sales-invoice-rows`, `/sales-invoices` | 2 |
| `sales_invoices::read` | `/sales-invoice-rows`, `/sales-invoices` | 4 |
| `sales_invoices::update` | `/sales-invoice-rows`, `/sales-invoices` | 2 |
| `subscription_products::create` | `/subscription-products` | 2 |
| `subscription_products::delete` | `/subscription-products` | 1 |
| `subscription_products::read` | `/subscription-products` | 2 |
| `subscription_products::update` | `/subscription-products` | 1 |
| `subscription_types::create` | `/subscription-types` | 3 |
| `subscription_types::delete` | `/subscription-type-items`, `/subscription-types` | 2 |
| `subscription_types::read` | `/subscription-type-items`, `/subscription-types` | 4 |
| `subscription_types::update` | `/subscription-type-items`, `/subscription-types` | 2 |
| `users::read` | `/users` | 2 |
| `work_types::create` | `/work-types` | 1 |
| `work_types::delete` | `/work-types` | 1 |
| `work_types::read` | `/work-types` | 2 |
| `work_types::update` | `/work-types` | 1 |
| `workflows::create` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 3 |
| `workflows::delete` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 3 |
| `workflows::read` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 6 |
| `workflows::update` | `/workflow-statuses`, `/workflow-transitions`, `/workflows` | 3 |
| None (a valid token is enough) | `/`, `/checkins`, `/furlough-balance-mutations`, `/me`, `/timers`, `/uploads`, `/worklogs` | 15 |

The drive operations name `project_drive::access` or `project_drive::create`. For items owned by a customer, the organisation or a user, plnnrly checks the matching `customer_drive`, `organization_drive` or `user_drive` permission instead.

### Operations that need no permission

A few operations need a valid token with the right scope and no permission at all. Their pages say so.

- [Discovery](https://developers.plnnrly.com/en/docs/api/v2/discovery/) and [GET /me](https://developers.plnnrly.com/en/docs/api/v2/me/get/).
- Reading and cancelling an upload ticket.
- Creating, changing and deleting your own worklogs, timers and check-ins, and reading your own furlough mutations. The record's own rules are the gate here, not a permission.

Other people's rows need a permission after all:

- worklogs: `times::update_others` or `times::delete_others`, and `times::update_others` to log time for someone else with `on_behalf_of_user_id`
- furlough mutations: `furlough::view_all` or `furlough::manage`

## Permissions that change what a call does

Some permissions do not decide whether a call runs, but what it may do.

**Fields with a permission of their own.** Sending one of these fields without its permission answers `403` [insufficient-permission](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#insufficient-permission) naming the field, except `is_overtime`, which is dropped instead.

| Field                  | On                 | Permission                 | Without it                                                                                     |
| ---------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------------------------- |
| `on_behalf_of_user_id` | worklogs           | `times::update_others`     | `403`                                                                                          |
| `debtor_code`          | customers          | `billing_settings::update` | `403`                                                                                          |
| `should_email`         | sales invoices     | `sales_invoices::finalize` | `403`                                                                                          |
| `is_approved`          | furlough mutations | `furlough::manage`         | `403`                                                                                          |
| `is_overtime`          | worklogs           | `times::write_overtime`    | Dropped, with an `input_ignored` warning in `meta.warnings`. The rest of the write is applied. |

A dropped field does not fail the call, so check `meta.warnings` after every write.

**Permissions that widen which rows you reach.** Without them you see and change only your own rows:

- `times::update_others` and `times::delete_others`: other people's worklogs
- `kanban::manage_for_others`: other people's kanban tasks
- `events::manage_for_others`: other people's agenda events
- `furlough::view_all` and `furlough::manage`: other people's furlough mutations

**Permissions a record's rules ask for.** A few writes check a further permission on the record you write to, and answer `403` [forbidden-by-policy](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#forbidden-by-policy) without it:

- `projects::notes`: adding a note to a project
- `times::write_on_others_tasks`: logging time on a task that belongs to someone else

Each operation page names the permissions that apply to it.

## What your organisation exposes

Call [GET /](https://developers.plnnrly.com/en/docs/api/v2/discovery/) with any API token. It answers one entry per resource your organisation has, each with its `permission_prefix`, so you can match it with the tables above.

Discovery only leaves out resources of features your organisation does not have. It does not check your token or your role, so a listed resource can still answer `403` for you.

## Related

- [API tokens](https://developers.plnnrly.com/en/docs/api/v2/guides/tokens/): creating a token with the right scopes.
- [Authentication](https://developers.plnnrly.com/en/docs/api/v2/guides/authentication/): the full order of checks.
- [Errors and retries](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/): every problem type and its fix.
