---
summary: Module permissions ScaiDial declares, what each gates, and how they map to
  roles. Plus the end-user portal's grant-based authorization model.
title: Permissions
path: reference/permissions
status: published
---

ScaiDial declares four module permissions. They're requested when an API key is minted and granted by a tenant admin.

| Permission | Gates |
|---|---|
| `scaidial:trunks:manage` | Trunks, DIDs, tenant policy (Settings page) |
| `scaidial:extensions:manage` | Extensions, extension grants, forward rules (admin-tier) |
| `scaidial:dialplan:manage` | Dialplans and dialplan rules |
| `scaidial:calls:observe` | Active calls, call history, leg controls (hangup / hold / transfer) |

A tenant-admin role typically gets all four. A read-only auditor might get only `:calls:observe`. A dialplan-editor role (useful for delegating routing changes without giving carrier credentials) gets only `:dialplan:manage`.

## How permissions are evaluated

The dependency injection layer (`require_module_permission`) reads the permission set on the authenticated user (JWT path) or the API key (key path). The check is strict: missing the right key returns 403 before the route body runs.

Super-admin bypasses all module permission checks. Cross-tenant access is separately blocked by `_require_tenant_scope` — even with the right module permission, a tenant admin can't see another tenant's trunks.

## End-user portal — no module permission, grant-based

The `/me/*` routes don't gate on `scaidial:*` permissions. They gate on `ExtensionGrant` rows.

| Grant role | What it allows on the granted extension |
|---|---|
| `owner` | Read everything + edit personal config (DND, forwarding, ring timeout, voicemail greeting). Place outbound calls from this extension. |
| `manage` | Read voicemail + call history. Used for admin-tier reassignment of someone else's line; not for day-to-day "where do my calls go". |
| `answer` | Receive ringing calls. Read voicemail (a delegated answerer needs to see what they missed). |
| `observe` | Read-only view of call history. Used for supervisors. |

A user with no grants on any extension lands on `/my/dial` and sees an empty state pointing them at their tenant admin.

## The matrix

| Operation | Permission needed | Or grant role |
|---|---|---|
| `GET /trunks` | `scaidial:trunks:manage` | — |
| `POST /trunks/{id}/resync` | `scaidial:trunks:manage` | — |
| `GET /dids` | `scaidial:trunks:manage` | — |
| `POST /extensions` | `scaidial:extensions:manage` | — |
| `POST /extensions/{id}/grants` | `scaidial:extensions:manage` | — |
| `GET /dialplans` | `scaidial:dialplan:manage` | — |
| `POST /dialplans/{id}/rules` | `scaidial:dialplan:manage` | — |
| `GET /calls/active` | `scaidial:calls:observe` | — |
| `POST /legs/{id}/hangup` | `scaidial:calls:observe` | — |
| `POST /legs/{id}/transfer` | `scaidial:calls:observe` | — |
| `GET /policy` | `scaidial:trunks:manage` | — |
| `PATCH /policy` | `scaidial:trunks:manage` | — |
| `GET /me/extensions` | (none) | any grant |
| `PATCH /me/extensions/{id}` | (none) | `owner` |
| `GET /me/voicemail` | (none) | `owner` or `manage` (per-row) |
| `GET /me/voicemail/{id}/audio` | (none) | `owner` or `manage` |
| `POST /me/click-to-call` | (none) | `owner` on the source extension |
| `POST /me/extensions/{id}/forward-rules` | (none) | `owner` |

## Issuing an API key for ScaiDial

API keys are minted in ScaiGrid Auth (Identity → API keys). On creation you select the module permissions to attach. For a typical tenant-admin key that should be able to fully manage ScaiDial:

```
scaidial:trunks:manage
scaidial:extensions:manage
scaidial:dialplan:manage
scaidial:calls:observe
```

Add `scaidial:diarize` separately if the tenant has ScaiEcho voicemail-transcript opt-in and you want the key to be able to request diarization on streaming endpoints elsewhere.

## Why no per-extension permission scheme

We considered `scaidial:extension:{id}:manage` as a separate permission key per extension. It would let you delegate ownership of specific extensions without giving the tenant-wide manage role. We chose grant rows instead because:

- Grants are a single index on `(extension_id, user_id)`, which scales linearly with users-times-extensions. A permission-per-extension would explode the permission table.
- Tenant admins already need an admin-tier permission for tenant-wide config; grants cover the per-extension delegation case directly.
- Grants compose with groups (a `group_id` instead of a `user_id`), which permissions don't.

If you need finer-grained admin delegation than the four module permissions allow, file an issue with the use case — we'll add it if there's demand.
