# Permissions and scope

API v3 authorization answers two questions, in this order: does the credential have the **permission** the operation requires, and is the requested resource within the **scope** of that permission? Without the permission, the API returns `403 permission_denied`. With the permission but outside its scope, the API returns `404 not_found`, the same response as for a resource that does not exist.

## Permissions

Permission codes follow the pattern `<plural resource>.<verb>`: `products.read`, `products.update`, `products.publish`, `users.create`, `webhooks.replay`. Retrieve the full catalog for your version over HTTP:

```
GET /api/v3/credentials/permission-catalog
```

Each entry includes the code, the resource type, a description, whether the permission can be delegated, and the release that introduced it. The catalog only grows: an existing code never changes meaning.

Three kinds of action require a dedicated permission rather than the generic write permission:

- **State transitions**: publishing a product requires `products.publish` in addition to `products.update`.
- **Sensitive fields**: personal user data in listings and reports requires `users.read_personal`. Without it, the key is omitted from the response.
- **High-impact operations**: revoking a certificate (`certificates.revoke`), replaying a webhook (`webhooks.replay`), and adjusting progress (`progress.adjust`).

Each group page in the [API reference](https://cademi.dev/api/reference.md) lists the exact permissions required by each operation.

## Policies, policy revisions, and selectors

A credential's access is defined by its policies. Together, they form a policy revision, and the one in effect is the active policy revision. You manage policies one at a time: add a policy (`POST /credentials/{credential_id}/policies`), update one (`PATCH /credentials/{credential_id}/policies/{policy_id}`), or remove one (`DELETE /credentials/{credential_id}/policies/{policy_id}`). Each change publishes a new policy revision that contains the full set of policies, and a policy keeps its `policy_id` across revisions.

When you update a policy, send the `ETag` from the retrieve operation in the `If-Match` header. If the credential changed in the meantime, the API returns `412 revision_mismatch` and publishes nothing.

Each policy combines permissions with resource selectors:

```json
{
  "capabilities": ["products.read", "modules.read", "lessons.read"],
  "resources": [
    { "type": "product", "selector": "ids", "ids": ["prd_12", "prd_34"] }
  ]
}
```

| Selector | What it covers |
|---|---|
| `instance` | The entire account, for permissions that have no resource of their own (settings, events, audit). |
| `all` | Every resource of that type in the account. With `include_future: false`, it is frozen to the IDs that exist at publication time. |
| `descendants` | Everything under a parent, set in `parent` (`module`, `lesson`, `exam`, `certificate`, and `comment` belong to `product`; `enrollment` belongs to `user`). With `include_future: false`, it is frozen the same way. |
| `ids` | An explicit list, with at most 1,000 IDs. |

The selector type must match the permission's resource type. A module permission accepts a module selector by `ids`, `descendants` of a product, `all`, or `instance`.

## How scope appears in responses

- **Listings**: the operation requires the permission with any selector, and the page contains only what is in scope. There is no error; the collection is smaller.
- **Single resources**: a resource outside the scope returns `404`.
- **Human mode**: the policy is always the credential's own. The administrator behind the access token determines authorship and eligibility, never additional access. See [Human mode (OAuth)](https://cademi.dev/api/oauth.md).

## Delegation

A credential with `credentials.policies.manage` can manage the policies of another credential, as long as what it grants is contained in what it has itself, and only with permissions marked as delegable in the catalog. Going beyond that returns `403 delegation_limit_exceeded`, and no new revision is published. Credential management permissions (`credentials.*`) and administrator permissions (`administrators.*`) are not delegable, and a credential cannot modify its own policies.

## Large policies

- Prefer `descendants` and `all` over ID lists, with `include_future: true`: the policy stays smaller, covers resources created later, and does not need to be republished.
- An `ids` selector with more than 1,000 IDs is rejected. If you want "everything that exists today", use `all` with `include_future: false` and let the API resolve the IDs. The same 1,000 limit applies to the resolved set.
- A revision with many policies is normal; what matters is the set, not the order.
- `GET /credentials/current/policies` returns the active policy revision of the calling credential, which is useful for debugging a `403` before you change anything.

## Rejections

| Code | When |
|---|---|
| `permission_denied` | The credential lacks the permission for the operation, field, or transition. |
| `not_found` | The credential has the permission, but the resource is outside its scope (or does not exist). |
| `delegation_limit_exceeded` | The requested policy goes beyond what the issuing credential has. |
| `validation_failed` | A permission is not in the catalog, a selector is incompatible, an ID does not exist, or a selector exceeds 1,000 IDs. |
| `revision_mismatch` | The `If-Match` value does not match the credential's current revision. |
