Team & roles
Invite people into your account, give each one a role, and let role-based access control (RBAC) decide what they can touch. Roles are just bundles of scopes; scopes are the same permission strings your API keys carry. Learn one model, use it everywhere.
All paths under
/v1/.Authorization: Bearer <api-key>required.
How it fits together
tenant ──< members (users) ──> role ──< scopes
▲
└── built-in (owner/admin/billing/member)
or custom (yours)
- A tenant is your account.
- A member is a user joined to your tenant through a membership. One membership = one user + one role.
- A role is a named bundle of scopes (e.g.
calls:write,billing:read). - A scope is a single permission string. The wildcard
*means "every scope."
Every privileged request is checked against a single scope. A member's
role decides which scopes they carry; an API key carries its own scope
list (chosen at issue time, defaulting to *). Same enforcement either
way — the permission string is the unit of authorization. See
Authentication for how API-key scopes are set.
Two scopes are also verification-gated: even with the right role,
they're refused until your tenant's email is verified (you'll get a
403). Those are flagged in the scope reference.
Members
A membership is the join between a user and your tenant. The
Membership resource never leaks the user's password or internal user
ID — only their email and role slug.
{
"id": "3f1c…-uuid",
"user_email": "dana@acme.com",
"role_slug": "admin",
"created_at": "2026-06-18T14:32:18.000Z"
}
Invite or add a member
POST /v1/members/
Content-Type: application/json
{
"email": "dana@acme.com",
"role_slug": "admin"
}
Both fields are required. role_slug must be a built-in role
(owner, admin, billing, member) or one of your custom roles.
What happens:
- If no user exists for that email, we create one and generate a password for them — it's delivered in the invite email, never in the API response.
- If the user already exists, they keep their own password and are simply joined to your tenant.
- An invite email goes out on success.
Response: 201 with the new Membership. Requires the
members:invite scope.
| Error | When |
|---|---|
400 member_invite_invalid | Missing email, unknown role_slug, or the user is already a member of this tenant. |
List members
GET /v1/members/?page=1&page_size=20
Returns a paginated list of Membership objects, newest first.
Requires members:read.
Change a member's role
PATCH /v1/members/{id}/
Content-Type: application/json
{ "role_slug": "billing" }
{id} is the membership UUID. The only field you can change is the
role. Requires members:role_change.
You cannot demote the only owner — promote another member to
owner first, or you'll get a 400.
| Error | When |
|---|---|
400 member_role_invalid | Unknown role_slug, or you tried to demote the last owner. |
404 | No membership with that id in your tenant. |
Remove a member
DELETE /v1/members/{id}/
Returns 204. Requires members:remove. You cannot remove the only
owner (400 member_remove_invalid) — every tenant must always have
at least one.
Roles
A role bundles scopes under a slug. Reading a role shows its scopes as
the permissions array (each entry is a scope string):
{
"id": "8a2d…-uuid",
"slug": "admin",
"name": "Administrator",
"description": "All operational permissions except tenant deletion and owner role changes.",
"is_system": true,
"permissions": ["account:read", "api_keys:write", "calls:write", "…"],
"created_at": "2026-06-18T14:32:18.000Z",
"updated_at": "2026-06-18T14:32:18.000Z"
}
is_system: true marks a built-in role (shared across all tenants).
is_system: false is one of your own custom roles. slug, id, and
is_system are read-only.
Built-in roles
Four roles ship with every tenant. They're shared system roles — visible to you but not editable or deletable.
| Slug | Name | Grants (summary) |
|---|---|---|
owner | Owner | Everything. Always holds every scope, including tenant:delete. Cannot be removed from the tenant. |
admin | Administrator | All operational scopes — members, roles, API keys, calls, numbers, billing, webhooks, connections, audit — except closing the tenant. |
billing | Billing | Read-only on the account plus full billing: top up, manage payment methods, view invoices. Read-only on calls, numbers, webhooks, connections. |
member | Member | Mostly read-only: view account, usage, members, roles. Can place and control calls (calls:read + calls:write) but not manage the account. |
The owner role automatically receives every scope, including new ones
added in future releases. Exact per-role grants are listed in the
scope reference.
List roles
GET /v1/roles/?page=1&page_size=20
Returns the four built-in roles plus your custom roles, paginated.
Requires roles:read.
Retrieve a role
GET /v1/roles/{id}/
{id} is the role UUID. Requires roles:read. 404 if the role
isn't a system role or one of yours.
Create a custom role
POST /v1/roles/
Content-Type: application/json
{
"name": "Support agent",
"description": "Place and review calls, read numbers.",
"permission_codes": ["calls:read", "calls:write", "numbers:read"]
}
| Field | Required | Notes |
|---|---|---|
name | yes | ≤120 chars. The slug is derived from this automatically. |
description | no | ≤255 chars. |
permission_codes | yes | Non-empty list of scope strings. Every code must exist (see the reference). |
Response: 201 with the new Role (is_system: false). Requires
roles:write. The slug is generated from name; if a role with that
slug already exists in your tenant you get a 400.
| Error | When |
|---|---|
400 role_invalid | Empty name, duplicate slug, empty permission_codes, or an unknown scope code (the message lists which). |
Update a custom role
PATCH /v1/roles/{id}/
Content-Type: application/json
{
"name": "Senior support",
"permission_codes": ["calls:read", "calls:write", "numbers:read", "recordings:delete"]
}
All three fields (name, description, permission_codes) are
optional — send only what you're changing. Passing permission_codes
replaces the role's entire scope set, not merges it. Requires
roles:write.
System roles cannot be modified — a PATCH against one returns 404
(it's not in your editable set).
Delete a custom role
DELETE /v1/roles/{id}/
Returns 204. Requires roles:delete. Two guards:
- System roles can't be deleted (
404— not in your set). - A role still assigned to members can't be deleted
(
400 role_invalid, "Role is in use; reassign members first"). Move those members to another role, then delete.
Scope reference
Every scope your roles and API keys can carry. The wildcard * grants
all of them. Gated scopes also require a verified tenant. The last
four columns show which built-in roles include each scope (Owner always
holds all of them).
| Scope | Allows | Gated | Admin | Billing | Member |
|---|---|---|---|---|---|
tenant:read | View own tenant | ✓ | ✓ | ✓ | |
tenant:update | Update tenant name / metadata | ✓ | |||
tenant:delete | Close own tenant | ✓ | |||
account:read | View account info, balance, quota | ✓ | ✓ | ✓ | |
quota:read | View own quota | ✓ | ✓ | ✓ | |
quota:update | Update own quota (lowering only) | ✓ | |||
usage:read | View usage history | ✓ | ✓ | ✓ | |
ledger:read | View ledger entries | ✓ | ✓ | ✓ | |
ledger:write | Top up balance | ✓ | ✓ | ✓ | |
api_keys:read | List API keys | ✓ | ✓ | ||
api_keys:write | Create API keys | ✓ | |||
api_keys:revoke | Revoke API keys | ✓ | |||
members:read | List members | ✓ | ✓ | ✓ | |
members:invite | Invite members | ✓ | |||
members:remove | Remove members | ✓ | |||
members:role_change | Change member roles | ✓ | |||
roles:read | List roles | ✓ | ✓ | ||
roles:write | Create / edit custom roles | ✓ | |||
roles:delete | Delete custom roles | ✓ | |||
audit:read | View audit log | ✓ | |||
numbers:read | List own phone numbers | ✓ | ✓ | ✓ | |
numbers:order | Order new phone numbers (debits balance) | ✓ | ✓ | ||
numbers:configure | Update phone number settings | ✓ | |||
numbers:release | Release a number back to inventory | ✓ | |||
pricing_tiers:read | View configured number pricing tiers | ✓ | ✓ | ✓ | |
billing:read | View payments, payment methods, plans | ✓ | ✓ | ✓ | |
billing:write | Top up, attach plan, manage payment methods | ✓ | ✓ | ✓ | |
calls:read | List and retrieve own calls / recordings | ✓ | ✓ | ✓ | |
calls:write | Place outbound calls, run in-call action verbs | ✓ | ✓ | ✓ | |
recordings:delete | Archive a recording | ✓ | ✓ | ||
webhooks:read | View webhook subscriptions and delivery log | ✓ | ✓ | ✓ | |
webhooks:write | Create, update, delete webhook subscriptions | ✓ | ✓ | ||
connection:read | View own connections (SIP / sub-customer scopes) | ✓ | ✓ | ✓ | |
connection:write | Create, rename, delete connections | ✓ | ✓ | ||
* | Wildcard — every scope above | — | — | — | — |
Use these strings verbatim in permission_codes when building a custom
role.
Best practices
- Least privilege. Start members on
memberand grant up. For anything finer than the built-ins, build a custom role with exactly the scopes the job needs. - Keep
ownerrare. Onlyownercan close the tenant. Give most admins theadminrole instead, and always keep at least two owners so you're never locked out. - Use API keys for machines, not shared logins. A member is a
person; a service should authenticate with its own scoped API key.
Issue a key with a narrow scope list (e.g.
["calls:write", "calls:read"]) rather than*, so a leak is contained. See Authentication. - Audit changes. Every invite, role change, removal, and role edit
is recorded. Pull the trail from
GET /v1/audit(needsaudit:read).
Errors
All errors use the standard envelope:
{ "error": { "code": "role_invalid", "message": "Role is in use; reassign members first." } }
The team-and-roles codes (member_invite_invalid,
member_role_invalid, member_remove_invalid, role_invalid) all
arrive as 400; 404 means the member or role isn't visible to your
tenant; 403 means your role or key lacks the required scope (or the
scope is verification-gated and your tenant isn't verified yet). Full
list: Error reference.
Next
- Authentication — issue scoped API keys and understand how scopes are enforced.
- Billing — what the
billingrole can manage. - Error reference — every code the API can return.
