> ## Documentation Index
> Fetch the complete documentation index at: https://flow9.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Access control over the API

> Read-only in this release: which roles and permission profiles a workspace has, what each one permits, and how far each role reaches into the data. Everything

Read-only in this release: which roles and permission profiles a workspace has,
what each one permits, and how far each role reaches into the data. Everything
takes `access_control:view`. Editing roles and grants waits for the authz
redesign; the AccessControl page remains the place to change them.

| Call                        | What it returns                                                                              |
| --------------------------- | -------------------------------------------------------------------------------------------- |
| `GET /v1/roles`             | Every role (as its system profile) and every custom profile, with active member counts.      |
| `GET /v1/roles/{id}`        | One of them with the full module × action matrix and, for a role, its data scope per module. |
| `GET /v1/visibility-scopes` | The role scope rules: role → module → `own` / `team` / `branch` / `company`.                 |

## The model these mirror

The CRM's authz engine does not decide from the legacy `role` table. Its unit
is the **permission profile**, per tenant:

* Each role has one **system profile** (`kind: "role"`, `is_system: true`) holding the
  role's default grants in that workspace. A user with the role gets those grants
  unless an explicit profile or a per-user override says otherwise.
* A **custom profile** (`kind: "profile"`, `role_id: null`) is assigned to individual
  users and replaces their role defaults.
* **Scope** (data reach) is a separate axis on the *role*, per module. It is what turns
  `leads:view` into "my leads" versus "every lead in the company". The same rules
  narrow an API key's reads to what its owner may see.

So an item of `GET /v1/roles` is a profile. Its `id` is what you pass to
`GET /v1/roles/{id}`; its `role_id` is the id you pass as `role_id` on
`POST /v1/users`. A legacy role that has members but no engine profile yet is still
listed (`has_permissions: false`, `id` = the role id) so an inventory is complete.
`Super_Admin` is the platform role and never appears.

## The permission matrix

`GET /v1/roles/{id}` returns `permissions[]` with one row per module — every module
of the vocabulary plus any dynamic `object:<slug>` module the profile mentions:

```json theme={null}
{ "module": "leads",
  "actions": ["view", "create", "edit"],
  "available_actions": ["view", "create", "edit", "delete"],
  "scope": "team" }
```

`available_actions` are the matrix columns, so a client can render the grid
without knowing the vocabulary. `scope` is `null` when no rule is set (the engine
reads that as `own`) and always `null` for a custom profile.

## Parity

This is the REST layer the read-only admin MCP tools consume; the
consent-time scope groups are unchanged. Effective permissions of one *user*
(role + profile + overrides) are not exposed here — that is `authz_user_effective`
in the app and a candidate for a later `GET /v1/users/{id}/access`.
