Permissions
GET /orgs/{orgId}/api/v1/admin/permissions
POST /orgs/{orgId}/api/v1/admin/permissions
GET /orgs/{orgId}/api/v1/admin/permissions/{permissionId}
PATCH /orgs/{orgId}/api/v1/admin/permissions/{permissionId}
DELETE /orgs/{orgId}/api/v1/admin/permissions/{permissionId}
GET /orgs/{orgId}/api/v1/admin/permissions/{permissionId}/usage
GET /orgs/{orgId}/api/v1/admin/scopes
POST /orgs/{orgId}/api/v1/admin/scopes
DELETE /orgs/{orgId}/api/v1/admin/scopes/{scopeId}
Permissions are fine-grained access controls identified by a slug (e.g., repo.write).
They can be assigned directly to users, groups, or roles.
Authentication
All permission and scope endpoints require an API key or a Bearer token with
admin:permissions:read (list, get, usage) or admin:permissions:write (create, update,
delete). See Authentication.
Each endpoint needs the credential's scope and a permission. A session or Bearer caller must
hold the permission; for an API key its creator must still hold it. A missing permission
returns 403 forbidden with details.required_permissions. See
Required permissions.
| Method | Endpoint | Permission | API-key / token scope |
|---|---|---|---|
| GET | /admin/permissions | roles.view | admin:permissions:read |
| GET | /admin/permissions/{permissionId} | roles.view | admin:permissions:read |
| POST | /admin/permissions | roles.assign-permissions | admin:permissions:write |
| PATCH | /admin/permissions/{permissionId} | roles.assign-permissions | admin:permissions:write |
| DELETE | /admin/permissions/{permissionId} | roles.assign-permissions | admin:permissions:write |
| GET | /admin/permissions/{permissionId}/usage | roles.view | admin:permissions:read |
| GET | /admin/scopes | roles.view | admin:permissions:read |
| POST | /admin/scopes | roles.assign-permissions | admin:permissions:write |
| DELETE | /admin/scopes/{scopeId} | roles.assign-permissions | admin:permissions:write |
The Permission Object
{
"id": 42,
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write",
"isSystem": false,
"isCustom": true,
"createdAt": "2026-01-15T10:30:00Z"
}
isCustom is true when the permission is organization-defined (rather than a built-in system permission).
System permissions (isSystem: true) are the same in every organization. They include the
admin catalogue (users.*, roles.*, settings.manage, zanzibar.*, abac.view,
abac.manage, gdpr.manage, gdpr.dpo, billing.manage, authz.check, and so on) and
jit.approve. jit.approve lets a user approve or deny just-in-time permission requests raised by AI
agents in the approval inbox. Organization administrators hold jit.approve
through the built-in admin role.
List Permissions
- curl
- Node
- Python
- Go
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions?page=1&limit=50" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions?page=1&limit=50", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions?page=1&limit=50",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions?page=1&limit=50", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Query Parameters
| Parameter | Description |
|---|---|
page | Page number (default: 1) |
limit | Results per page (default: 25, max: 100) |
search | Filter by name, slug, or description |
resource | Filter by the permission's resource value |
sortBy, sortDir | Sort field and direction (asc/desc) |
{
"data": [
{
"id": 42,
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write",
"isSystem": false,
"isCustom": true,
"createdAt": "2026-01-15T10:30:00Z"
}
],
"meta": {
"total": 18,
"page": 1,
"limit": 25,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
},
"resourceType": "permissions"
}
Create Permission
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write"
}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write"
})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write"
}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{
"slug": "repo.write",
"name": "Write to Repository",
"description": "Allows writing to code repositories",
"resource": "repos",
"action": "write"
}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Slug rules
The slug is validated before anything is written. A slug is accepted only if it:
- uses lowercase letters and digits, separated by single
.,-or_characters (pattern^[a-z0-9]+(?:[._-][a-z0-9]+)*$, at most 100 characters), for examplereports.vieworrepo-write; - contains no wildcard (
*,**,users.*), because a wildcard slug would match every permission in checks; - is not the slug of a built-in system permission (for example
users.view,settings.manage,jit.approve); - does not sit inside a system namespace, that is, any slug starting with a system
resource such as
users.,billing.,gdpr.orabac.(for exampleusers.impersonate); - does not start with
tenants., which is reserved for platform administration; - is not a retired slug that the platform once checked:
agents.manage,users.manage,zanzibar.manage,webhooks.test,gdpr.export_other,abac.policies.*andabac.attributes.*.
The same reserved list applies in the portal and to the authoring of AI policies. Custom slugs are unique per organization (another organization's custom permissions are neither visible nor blocking). Reserving the system slugs and namespaces stops an organization from creating in advance a permission that the platform seeds later.
| Problem | Response |
|---|---|
| Malformed, wildcard, too long or reserved slug | 400 invalid_request; message explains the rule |
| Slug already exists in this organization or as a system permission | 409 conflict |
The colon form repo:write is not accepted; use repo.write or repo-write. The
permission resource and action fields are free text and do not have to match the slug.
Update Permission
- curl
- Node
- Python
- Go
curl -X PATCH https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42 \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"description": "Allows writing and force-pushing to code repositories"
}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42", {
method: "PATCH",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
"description": "Allows writing and force-pushing to code repositories"
})
});
console.log(await resp.json());
import os, requests
resp = requests.patch(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={
"description": "Allows writing and force-pushing to code repositories"
}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{
"description": "Allows writing and force-pushing to code repositories"
}`)
req, _ := http.NewRequest("PATCH", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Get Permission Usage
Returns the roles that have been assigned this permission.
- curl
- Node
- Python
- Go
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42/usage" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42/usage", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42/usage",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42/usage", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
{
"data": {
"roles": [
{ "id": 17, "name": "developer", "slug": "developer" }
],
"roleCount": 1
}
}
Delete Permission
- curl
- Node
- Python
- Go
curl -X DELETE "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/permissions/42", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
System permissions (isSystem: true) cannot be deleted.
OAuth 2.0 Scopes
Custom scopes are distinct from permissions — they are included in OAuth access tokens and control API access granted to OAuth clients.
Scope names are unique per organization. POST /admin/scopes returns 409 conflict when
the name already exists in this organization or matches a system scope (openid, profile,
email, admin, admin:users:read, and so on). Two organizations may define the same custom
scope name independently. See Scopes for the full catalogue.
List Scopes
- curl
- Node
- Python
- Go
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Create Scope
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "read:reports",
"description": "Read financial reports",
"isDefault": false
}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
"name": "read:reports",
"description": "Read financial reports",
"isDefault": false
})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={
"name": "read:reports",
"description": "Read financial reports",
"isDefault": False
}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{
"name": "read:reports",
"description": "Read financial reports",
"isDefault": false
}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
{
"error": "conflict",
"message": "A scope with this name already exists",
"status": 409
}
Delete Scope
- curl
- Node
- Python
- Go
curl -X DELETE "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes/7" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes/7", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes/7",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/scopes/7", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))