---
title: Roles and Permissions
path: concepts/roles-and-permissions
status: published
---

# Roles and Permissions

ScaiSend uses role-based access control. Users are assigned roles; roles carry permissions; permissions gate endpoints. API keys bypass the user layer — they carry scopes directly — but are gated by the same permission set.

## Permissions

A permission is a simple string like `mail.send`. Every endpoint declares the permission it requires. If the active credential lacks it, the request returns `403 Forbidden`.

The full set:

| Permission | Category | What it allows |
|------------|----------|----------------|
| `mail.send` | mail | Send mail via `POST /v3/mail/send`. |
| `mail.schedule` | mail | Use `send_at` (future) or batch grouping on send. |
| `mail.cancel` | mail | Cancel a queued or processing message. |
| `templates.read` | templates | List and read templates and versions. |
| `templates.write` | templates | Create and update templates and versions. |
| `templates.delete` | templates | Delete templates and versions. |
| `suppressions.read` | suppressions | List bounces, spam reports, unsubscribes, groups. |
| `suppressions.write` | suppressions | Add/remove suppressions, create/edit groups. |
| `stats.read` | stats | Read stats, category breakdowns, totals. |
| `stats.export` | stats | Trigger stats exports. |
| `webhooks.read` | webhooks | List webhook endpoints and event settings. |
| `webhooks.write` | webhooks | Create, update, delete webhook endpoints. |
| `domains.read` | domains | List sender domains. |
| `domains.write` | domains | Add domains, verify DNS, rotate DKIM keys. |
| `admin.api_keys` | admin | Create, update, and revoke API keys. |
| `admin.users` | admin | Assign and remove user-role mappings. |
| `admin.settings` | admin | Edit tenant settings (tracking, defaults). |

Query the live list:

```bash
curl https://scaisend.scailabs.ai/v3/scopes \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

## Default roles

Three roles are created for every new tenant. You can use them as-is, modify their permission sets, or add custom roles.

| Role | Intended for | Permissions |
|------|--------------|-------------|
| `admin` | Tenant operators | All of the above. |
| `developer` | Application integrators | `mail.send`, `mail.schedule`, `templates.read`, `stats.read`, `webhooks.read`. |
| `viewer` | Support agents, auditors | `templates.read`, `stats.read`, `suppressions.read`. |

The default roles are created by `scaisend sync --create-roles` on install. After that, they're ordinary tenant-scoped roles you can rename, re-scope, or delete.

## Assigning roles to users

A user can hold any number of roles. Role assignments are per-tenant — a user who operates on two tenants will have separate role assignments in each.

```bash
# As a JWT with admin.users
curl -X POST https://scaisend.scailabs.ai/api/admin/users/usr_ada/roles/role_developer \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

Or from the CLI (use this when bootstrapping):

```bash
scaisend assign-role ada@example.com --role developer --tenant acme-corp
```

## Group-to-role mappings

When a user is in a ScaiKey group, you can map the group to a role and every member inherits the role's permissions.

```bash
curl -X POST https://scaisend.scailabs.ai/api/admin/group-mappings \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"group_id": "grp_engineering", "role_id": "role_developer"}'
```

```python
import os
import httpx

httpx.post(
    "https://scaisend.scailabs.ai/api/admin/group-mappings",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_JWT']}"},
    json={"group_id": "grp_engineering", "role_id": "role_developer"},
)
```

```typescript
await fetch("https://scaisend.scailabs.ai/api/admin/group-mappings", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_JWT}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ group_id: "grp_engineering", role_id: "role_developer" }),
});
```

Group mappings stack with explicit role assignments — a user gets the union of permissions from both. **Nested groups inherit**: if `Backend Team` is a child of `Engineering`, and `Engineering` maps to `developer`, every member of `Backend Team` gets `developer` too.

This is the intended pattern for organizations that manage access through group membership in their identity provider. Map the groups once; user onboarding happens via the IdP.

## API keys and scopes

API keys carry the same permission strings as roles, stored as scopes on the key.

```bash
curl -X POST https://scaisend.scailabs.ai/v3/api_keys \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production-sender",
    "environment": "live",
    "scopes": ["mail.send"]
  }'
```

The key gets exactly those scopes. It doesn't inherit additional permissions from the user who created it, and it can't be widened later without `admin.api_keys` access.

**A key can never grant more than the creator has.** If the creator has `mail.send` but not `templates.write`, they can't mint a key with `templates.write`.

## Designing your permission model

Some rules of thumb that hold up in practice:

- **Give keys the narrowest scope that works.** An application that only sends mail gets `mail.send`, nothing else. If it's compromised, the blast radius is limited to sending mail — no template manipulation, no API key creation, no webhook rewiring.
- **Use separate keys for separate applications.** Rotate one without disrupting the others.
- **Don't use a key for human operations.** If you need to poke around in a terminal, use your JWT. Keys are for non-interactive automation.
- **Gate template editing to `templates.write` holders only.** Templates can render arbitrary HTML into every email — in effect, they're code. Treat their authorship the way you'd treat production code.
- **Map groups to roles, not users to roles.** Fewer things to keep in sync. Users churn; groups don't.

## Checking effective permissions

A user can see their own effective permissions:

```bash
curl https://scaisend.scailabs.ai/v3/auth/me \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

Response includes `permissions: [...]` — the union of all permissions granted by direct role assignments and by group mappings.

For an API key, list it:

```bash
curl https://scaisend.scailabs.ai/v3/api_keys \
  -H "Authorization: Bearer $SCAISEND_JWT"
```

Each key's `scopes` field is its effective permission set.

## Custom roles

Create a new role, then attach permissions:

```bash
# 1. Create the role
curl -X POST https://scaisend.scailabs.ai/api/admin/roles \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"name": "billing-agent", "description": "Read stats and resend invoices"}'

# 2. Attach a full permission set
curl -X PUT https://scaisend.scailabs.ai/api/admin/roles/role_billing_agent/permissions \
  -H "Authorization: Bearer $SCAISEND_JWT" \
  -H "Content-Type: application/json" \
  -d '{"permission_ids": ["perm_stats_read", "perm_mail_send", "perm_templates_read"]}'
```

Roles are tenant-scoped — a role created in tenant A doesn't exist in tenant B. Use consistent names if you're provisioning the same set of roles across tenants.

## What's next

- [Authentication](authentication) — using the credentials these permissions gate.
- [Multi-tenancy](multi-tenancy) — how tenants and partners fit together.
- [Admin Reference](../reference/admin) — every admin endpoint.
