# Scopes & Permissions

The permission system that governs sessions, Personal Access Tokens, and OAuth clients — and how to pick the right scopes for your integration

Every authenticated request carries a permission set. Sessions get the full set of user-facing
permissions; Personal Access Tokens (PATs) and OAuth client tokens get only the scopes you grant
at creation time. A request missing a required scope fails with `INSUFFICIENT_PERMISSIONS`.

## Quick Reference

All user-facing scopes, grouped by the resource they govern.

| Category     | Scopes                                                                                                            |
| ------------ | ----------------------------------------------------------------------------------------------------------------- |
| User profile | `user:read`, `user:write`                                                                                         |
| Projects     | `projects:read`, `projects:write`                                                                                 |
| Subscription | `subscription:read`, `subscription:write`                                                                         |
| API keys     | `api-keys:read`, `api-keys:write`, `api-keys:delete`                                                              |
| Organization | `organization:read`, `organization:manage-members`, `organization:manage-billing`, `organization:manage-security` |

These are the scopes you can include in a PAT's `metadata.scopes` array or request from an OAuth client. Organization scopes must be requested explicitly and are then capped by the caller's current role in that organization.

## How scopes are assigned

### Session users

When a user signs in, their session carries the full set of user-facing permissions listed above. You cannot narrow a session's scopes — narrowing happens at the UI layer.

### Personal Access Tokens

When you create a PAT via `POST /api/auth/api-key/create`, you pass an explicit `metadata.scopes` array. The token can never exceed the user's own permissions, and any scope you leave out is blocked even if the owning user has it. This is the mechanism you use to lock down a CI bot.

```json
POST /api/auth/api-key/create
{
"name": "Build status poller",
"metadata": { "scopes": ["projects:read", "subscription:read"] },
"expiresIn": 2592000
}
```

### OAuth clients

OAuth clients request scopes during the Authorization Code flow. The end user approves them at consent time, and the issued access token carries exactly those scopes.

## Scope Reference

### user:read

Read the signed-in user's account, login history, email preferences, and data export.

**Endpoints:**

- `GET /api/user/me`
- `GET /api/user/me/login-history`
- `GET /api/user/me/email-preferences`
- `GET /api/user/me/export`

### user:write

Update email preferences and account state (deletion, recovery). Product identity is email-only — there is no profile/handle/avatar mutation.

**Endpoints:**

- `PUT /api/user/me/email-preferences`
- `POST /api/user/me/recover`
- `DELETE /api/user/me`

### projects:read

List and read projects and Indexhog operational resources inside an organization (tenant route — the organization id is part of the URL path).

**Endpoints (representative):**

- `GET /api/user/organizations/{organizationId}/projects`
- `GET /api/user/organizations/{organizationId}/projects/:id`
- `GET /api/user/organizations/{organizationId}/projects/:id/verification`
- `GET /api/user/organizations/{organizationId}/projects/:id/urls`
- `GET /api/user/organizations/{organizationId}/projects/:id/issues`
- `GET /api/user/organizations/{organizationId}/projects/:id/stats`

### projects:write

Create and update projects, manage verification, inventory, imports, issue status, and engine connections.

**Endpoints (representative):**

- `POST /api/user/organizations/{organizationId}/projects`
- `PUT /api/user/organizations/{organizationId}/projects/:id`
- `DELETE /api/user/organizations/{organizationId}/projects/:id`
- `PATCH /api/user/organizations/{organizationId}/projects/:id/status`
- `POST /api/user/organizations/{organizationId}/projects/:id/verification/*`
- `POST /api/user/organizations/{organizationId}/projects/:id/urls`
- `POST /api/user/organizations/{organizationId}/projects/:id/imports`
- `PATCH /api/user/organizations/{organizationId}/projects/:id/issues/:issueId`

### subscription:read

Read the current organization's subscription state, plan, and usage counters.

**Endpoints:**

- `GET /api/user/organizations/{organizationId}/payments/subscription`
- `GET /api/user/organizations/{organizationId}/payments/subscription/usage`

### subscription:write

Start checkouts, change subscription details (including seat count), buy one-time add-ons, and open the customer billing portal. Most write endpoints additionally require the requested `organization:manage-billing` scope and an owner role.

**Endpoints:**

- `POST /api/user/organizations/{organizationId}/payments/checkout`
- `POST /api/user/organizations/{organizationId}/payments/verify`
- `PATCH /api/user/organizations/{organizationId}/payments/subscription`
- `GET /api/user/organizations/{organizationId}/payments/portal`

### organization:read

Read organization details, members, security requirements, verified domains, and subscription usage. A delegated credential must request this scope; membership alone does not add it.

### organization:manage-members

Update organization settings and membership: invitations, roles, removals, ownership transfer, and member-facing MFA policy. The effective permission is also capped by the caller's organization role.

### organization:manage-billing

Manage billing settings, subscription commands, purchase intents, and verified domains. This scope is owner-only after the role ceiling is applied.

### organization:manage-security

Change organization security configuration such as SSO. This scope is owner-only after the role ceiling is applied.

### api-keys:read

List OAuth applications and PATs belonging to the signed-in user.

**Endpoints:**

- `GET /api/user/oauth-clients`
- `GET /api/user/oauth-clients/:clientId`
- `GET /api/auth/api-key/list`
- `GET /api/auth/api-key/get`

### api-keys:write

Create and update OAuth applications and PATs.

**Endpoints:**

- `POST /api/auth/oauth2/register`
- `PUT /api/user/oauth-clients/:clientId`
- `POST /api/user/oauth-clients/:clientId/regenerate-secret`
- `POST /api/user/oauth-clients/:clientId/toggle`
- `POST /api/auth/api-key/create`
- `POST /api/auth/api-key/update`

### api-keys:delete

Delete OAuth applications and revoke PATs.

**Endpoints:**

- `DELETE /api/user/oauth-clients/:clientId`
- `POST /api/auth/api-key/delete`

## Common scope sets

### Read-only dashboard

For a UI that displays projects and subscription status but never writes.

```text
["user:read", "projects:read", "subscription:read", "organization:read"]
```

### CI bot that manages projects

For a deploy bot that creates and updates project records and inventory.

```text
["projects:read", "projects:write", "organization:read"]
```

### Billing automation

For a service that manages seat counts, credit top-ups, and renewals on behalf of an organization.

```text
["subscription:read", "subscription:write", "organization:read", "organization:manage-billing"]
```

### Token manager (lifecycle only)

For tooling that provisions and rotates PATs on behalf of a user.

```text
["api-keys:read", "api-keys:write", "api-keys:delete"]
```

## Organization pinning

PATs may be pinned to a single organization by passing `metadata.organizationId` at creation time. A pinned token rejects any request whose URL `:organizationId` does not match the pin — even if the owner is a member of other organizations.

```json
POST /api/auth/api-key/create
{
"name": "Acme prod bot",
"metadata": {
  "scopes": ["projects:read", "projects:write", "organization:read"],
  "organizationId": "01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
}
}
```

Use this for any token that should only ever touch one tenant. It is a far smaller blast radius than relying on the caller to send the right URL.

## Scope enforcement

When a request lacks a required scope, the API returns:

```json
{
"success": false,
"status": 403,
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Insufficient permissions. Required: projects:write",
"meta": {}
}
```

This is a hard failure — retrying will not fix it. Either issue a new token with the missing scope or sign in as a user who has it.

> **Warning:** **Scopes cannot be changed on an existing PAT after creation.** The `POST
>   /api/auth/api-key/update` endpoint updates name and enabled status only. To add a scope, revoke
> the token and create a new one.

## Best Practices

- **Least privilege always:** The correct number of scopes on a token is the smallest set that makes the automation work. Err on fewer and add later.
- **Pair reads and writes deliberately:** A bot that only needs `projects:read` should never get `projects:write`, even "just in case."
- **Pin production tokens to an organization:** Accidents at the URL layer become `FORBIDDEN` responses instead of cross-tenant writes.
- **Rotate on schedule:** Pass `expiresIn` (in seconds) so tokens die on their own. Build rotation into CI secrets management.
- **Never log the `key` value:** The `tkn_*` prefix makes them easy to grep for — do not hand leakers the job.
- **One automation, one token:** Revocation becomes surgical instead of catastrophic.

## Next Steps

1. **Set up SSO for your team:** Follow [Enterprise SSO](/docs/get-started/enterprise-sso) to require corporate identity for domain users.
2. **Issue your first token:** Walk through [Quick Start](/docs/get-started/quick-start) to create one with the scopes you just picked.
3. **Plan for rate limits:** See [Rate Limits](/docs/get-started/rate-limits) before running anything in a loop.
4. **Browse endpoints:** Open the [API Keys reference](/docs/api-reference/api-keys) or the [Projects reference](/docs/api-reference/projects) to start integrating.