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, which 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 (any slug starting with a system resource such asusers.,billing.,gdpr.orabac., for exampleusers.impersonate) and does not start withtenants., which is reserved for platform administration; - is not a retired slug 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 AI policy authoring. 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 pre-empting a permission 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))