{"openapi":"3.0.0","paths":{"/api/v1/permissions":{"get":{"description":"Returns the permission catalog for the active Application within the current Environment as an unpaginated `{ items }` collection. Use the optional `source` query parameter to filter to `system` (built-in) or `custom` (caller-defined) permissions; omit it to return both. Requires the `rbac.view_roles` permission.","operationId":"ApiPermissionsController_listPermissions","parameters":[{"name":"source","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Permissions returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PermissionResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List permission catalog","tags":["api-permissions"]},"post":{"description":"Registers one or more custom permissions in the active Application's catalog for the current Environment, accepting an array under `permissions` (each with a required `key`, `name`, and `category`, plus an optional `description`). All created permissions are marked `source: custom`; a key that already exists in the Environment returns `409`. Requires the `rbac.manage_roles` permission and emits a `permission.created` audit event per key.","operationId":"ApiPermissionsController_createPermissions","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePermissionsDto"}}}},"responses":{"201":{"description":"Permission(s) created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PermissionResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Register new permission(s)","tags":["api-permissions"]}},"/api/v1/permissions/{id}":{"get":{"description":"Returns a single permission by `id`, scoped to the active Application and current Environment. Returns `404` when no permission with that id exists in the Environment. Requires the `rbac.view_roles` permission.","operationId":"ApiPermissionsController_getPermission","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Permission returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PermissionResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get a permission","tags":["api-permissions"]},"patch":{"description":"Updates a permission's `name`, `description`, and/or `category` by `id` (`description` accepts `null` to clear; `name` and `category` can be changed but never cleared); the permission `key` and `source` are immutable. Supports optimistic concurrency via the `If-Match` header carrying the permission's current version — a stale version returns `409`. Requires the `rbac.manage_roles` permission.","operationId":"ApiPermissionsController_updatePermission","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePermissionDto"}}}},"responses":{"200":{"description":"Permission updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PermissionResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update permission metadata","tags":["api-permissions"]},"delete":{"description":"Deletes a custom permission by `id`, scoped to the current Environment. System permissions (`source: system`) are protected and cannot be deleted. Supports optimistic concurrency via the `If-Match` header (`409` on a version mismatch). Responds `204 No Content` on success and requires the `rbac.manage_roles` permission.","operationId":"ApiPermissionsController_deletePermission","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"responses":{"204":{"description":"Permission deleted"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Remove a permission","tags":["api-permissions"]}},"/api/v1/permissions/{id}/usage":{"get":{"description":"Returns the roles that grant a permission, each with the number of distinct identities holding that role, plus `role_count` and the distinct `identity_count` the permission reaches overall. Because a permission is only ever held through a role, this is the full blast radius of deleting it — the listed roles are exactly the ones a delete would strip it from. Deactivated roles are included; `identity_count` is not the sum of the per-role counts, since one identity may hold several granting roles. Returns `404` when no permission with that id exists in the Environment. Requires the `rbac.view_roles` permission.","operationId":"ApiPermissionsController_getPermissionUsage","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Granting roles and identity reach returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PermissionUsageDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"404":{"description":"No permission with that id exists in this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No permission with that id exists in this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/{id}/usage","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get where a permission is used","tags":["api-permissions"]}},"/api/v1/permissions/evaluate":{"post":{"description":"Evaluates whether a target identity (`identity_id`) holds a `permission`, returning a boolean decision plus the `scope_evaluated`, `effective_node_id`, `granting_roles`, and any `denial_reason`. With `scope: node` the engine walks the node's hierarchy lineage so a permission granted on an ancestor is inherited at `node_id`; `scope: app_wide` answers the coarse 'anywhere in the Application' question (returns `effective_node_id: null`) and must never be used for resource-scoped enforcement. Scoped API keys must carry the evaluate scope or the call returns `403`. Rate-limited to 1000 requests/minute.","operationId":"ApiPermissionsController_evaluate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluatePermissionDto"}}}},"responses":{"200":{"description":"Authorization decision returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EvaluateResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/evaluate","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Evaluate authorization","tags":["api-permissions"]}},"/api/v1/permissions/evaluate/bulk":{"post":{"description":"Evaluates many permission checks in a single request, returning a `{ items }` array of decisions in the same order as the input `checks`, each with the same shape as the single evaluate endpoint. Every check is resolved independently against its own `identity_id`, `permission`, and `scope` (`node` lineage walk or `app_wide`). Scoped API keys must carry the evaluate scope (`403` otherwise). Rate-limited to 500 requests/minute.","operationId":"ApiPermissionsController_evaluateBulk","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkEvaluatePermissionDto"}}}},"responses":{"200":{"description":"Authorization decisions returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/EvaluateResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/evaluate/bulk","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk evaluate authorization","tags":["api-permissions"]}},"/api/v1/permissions/evaluate/explain":{"post":{"description":"Returns a full authorization trace explaining why an identity is or isn't granted a `permission` at a `node_id`. In addition to the verdict (copied verbatim from the evaluate engine), the response includes the resolved `lineage` from the node up its ancestors, each node's role assignments, and which assignments actually grant the permission — useful for debugging inherited access. Returns `404` when the identity or node cannot be found; scoped API keys must carry the evaluate scope (`403`). Rate-limited to 200 requests/minute.","operationId":"ApiPermissionsController_explain","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainPermissionDto"}}}},"responses":{"200":{"description":"Authoritative verdict plus a root-first lineage breakdown showing where the permission is granted or denied","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PermissionTraceDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions","method":"GET"}}}}},"403":{"description":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"Scoped API key is missing the `permissions.evaluate` scope (full_access keys and admin JWTs are unrestricted)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/evaluate/explain","method":"POST"}}}}},"404":{"description":"The `node_id` does not exist in this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"The `node_id` does not exist in this environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/permissions/evaluate/explain","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Explain a node-scoped authorization decision","tags":["api-permissions"]}},"/api/v1/identities":{"get":{"description":"Returns a paginated list of identities that hold an active membership in the active Application's current Environment. Each row carries profile metadata only (id, email, name, `external_id`, `is_active`, `created_at`) — no credentials or assignment data. Supports `q` full-text search across first name, last name, and email, an optional `active`/`inactive` status filter, plus the standard paging and sorting query parameters.","operationId":"ApiIdentitiesController_listIdentities","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List identities in Application","tags":["api-identities"]},"post":{"description":"Creates a single identity in the active Application's current Environment, optionally assigning an initial role at a node in the same request (`role_id` + `node_id`). Email is normalized (lower-cased, trimmed) and must be unique within the Account — a duplicate returns `409`; passwords are hashed with argon2id and screened against HaveIBeenPwned, so a breached password returns `400`. Returns the created identity with profile metadata only (the password is never echoed).","operationId":"ApiIdentitiesController_createIdentity","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIdentityDto"}}}},"responses":{"201":{"description":"Identity created (optionally with initial role assignment)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityResponseDto"}}}}}},"400":{"description":"Password rejected — appeared in a known data breach (HaveIBeenPwned check)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Password rejected — appeared in a known data breach (HaveIBeenPwned check)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"409":{"description":"Email already exists in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Email already exists in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create an identity","tags":["api-identities"]}},"/api/v1/identities/with-roles":{"get":{"description":"Returns a paginated list of the current Environment's identities, each enriched with its role assignments (role id/name plus the node id/name the role is held at). Filter with `role_id` to return only identities holding a given role, or the sentinel `no_assignment` to return identities with no assignments in this Environment; `q` searches name and email and `status` narrows to active or inactive. Identities with no assignments come back with an empty `roles` array.","operationId":"ApiIdentitiesController_listIdentitiesWithRoles","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"role_id","required":false,"in":"query","description":"Filter by role ID (UUID) or \"no_assignment\" for unassigned identities","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by active/inactive status. Omit to show all.","schema":{"type":"string","enum":["active","inactive"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityRowDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List identities with their role assignments","tags":["api-identities"]}},"/api/v1/identities/summary":{"get":{"description":"Returns aggregate identity counts for the current Environment: `total_identities` (active memberships), `assigned_count` (identities holding at least one role assignment in this Environment), and `unassigned_count`. Intended for dashboard headers and onboarding checks rather than per-identity inspection.","operationId":"ApiIdentitiesController_getIdentitiesSummary","parameters":[],"responses":{"200":{"description":"Identity counts returned (total, assigned, unassigned)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentitiesSummaryDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get Application identities summary","tags":["api-identities"]}},"/api/v1/identities/bulk-create":{"post":{"description":"Creates many identities in one request as a partial-success operation: each row is processed independently, so per-row validation, duplicate-email (`409`), and breached-password (`400`) failures fold into a `failures` array instead of aborting the batch. Returns `200` when every row succeeds and `207 Multi-Status` when any row fails, with `successes` and `failures` keyed by request index; same-batch duplicate emails are reported as per-row conflicts. Authorization is evaluated once up front. Supply an `Idempotency-Key` header to make retries safe — a replayed key returns the original result rather than re-creating rows.","operationId":"ApiIdentitiesController_bulkCreateIdentities","parameters":[{"name":"Idempotency-Key","in":"header","description":"Optional opaque string the server uses to deduplicate retried requests. When the same key is replayed within 24 hours, the original response (status + body) is returned without re-processing — eliminates duplicate writes from network blips between server commit and client receipt. Generate one per logical operation (UUIDv4 recommended). Max 255 characters.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateIdentitiesDto"}}}},"responses":{"200":{"description":"Per-row identity creation. Each row is processed independently — duplicates and validation errors fail individually without aborting the batch. Returns 200 on full success or 207 Multi-Status on mixed outcomes; the body is always { summary, results } so callers iterate the same way regardless. — all items succeeded","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/IdentityResponseDto"}}}}}}}}},"207":{"description":"Per-row identity creation. Each row is processed independently — duplicates and validation errors fail individually without aborting the batch. Returns 200 on full success or 207 Multi-Status on mixed outcomes; the body is always { summary, results } so callers iterate the same way regardless. — mixed outcomes (one or more items failed)","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"oneOf":[{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/IdentityResponseDto"}}},{"type":"object","required":["index","status","code","input","error"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["error"]},"code":{"type":"integer"},"input":{"type":"object"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","nullable":true,"description":"Machine-readable error code"},"message":{"type":"string","description":"English fallback message"},"details":{"description":"Optional validation error details"}}}}}]}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk-create identities","tags":["api-identities"]}},"/api/v1/identities/{id}":{"get":{"description":"Returns a single identity by id, scoped to the current Environment. The response carries profile metadata only (id, email, name, `external_id`, `is_active`, `created_at`); use the detail endpoint for role assignments. Returns `404` when the id does not resolve to an identity with membership in this Environment.","operationId":"ApiIdentitiesController_getIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Identity profile returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get an identity","tags":["api-identities"]},"patch":{"description":"Updates an identity's mutable profile fields (`first_name`, `last_name`, `is_active`, `metadata`) in the current Environment. `email` and `external_id` are immutable here — `external_id` is owned by the SSO/SCIM provisioning path and cannot be re-pointed through this endpoint. Returns the updated identity and writes an `identity.updated` audit row; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_updateIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIdentityDto"}}}},"responses":{"200":{"description":"Identity updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update an identity","tags":["api-identities"]},"delete":{"description":"Removes an identity from the current Environment without hard-deleting the Identity row (the same person may belong to other Environments in the Account). The Environment membership is flipped to `deactivated` so it stays reversible, every role assignment at nodes in this Environment is dropped, and any still-pending invite for that email into this Environment is revoked. Assignments and memberships in other Environments are untouched. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_removeIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Identity removed (assignments cascade deleted)"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Remove an identity","tags":["api-identities"]}},"/api/v1/identities/{id}/detail":{"get":{"description":"Returns an enriched view of a single identity in the current Environment: profile fields plus `email_verified`, `metadata`, and the identity's role assignments resolved to role and node names. Also returns `can_manage`, reflecting whether the calling principal holds `identity.manage` in this Application. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Identity detail with roles and management flags returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityDetailResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/detail","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get identity detail with role assignments","tags":["api-identities"]}},"/api/v1/identities/{id}/activate":{"post":{"description":"Reactivates an identity by setting `is_active = true` in the current Environment, allowing it to authenticate again. Returns a `200` message envelope. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_activateIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Identity activated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/activate","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Activate an identity","tags":["api-identities"]}},"/api/v1/identities/{id}/deactivate":{"post":{"description":"Deactivates an identity by setting `is_active = false` in the current Environment, blocking future sign-in. Returns a `200` message envelope. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_deactivateIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Identity deactivated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/deactivate","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Deactivate an identity","tags":["api-identities"]}},"/api/v1/identities/{id}/password":{"post":{"description":"Sets an identity's password directly (server-to-server admin), primarily for migrating users off a legacy store without the user present. Reuses the argon2id + HaveIBeenPwned path, so a breached password returns `400`; the prior hash is recorded to password history, any active failed-login lockout is cleared, and the identity's active sessions are revoked so a rotated credential can't keep a stale session alive. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_setIdentityPassword","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetIdentityPasswordDto"}}}},"responses":{"204":{"description":"Password set. The identity's existing sessions are revoked and any active failed-login lockout is cleared; the new password takes effect on next sign-in. Intended for backend/admin tooling (e.g. migrating users off a legacy store)."},"400":{"description":"Password rejected — appeared in a known data breach (HaveIBeenPwned check)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Password rejected — appeared in a known data breach (HaveIBeenPwned check)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/password","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/password","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Set an identity's password (admin)","tags":["api-identities"]}},"/api/v1/identities/{id}/password/reset":{"post":{"description":"Triggers the soft password-reset path for an identity: emails a reset link valid for one hour while leaving the current password and existing sessions intact until the identity completes the reset. The email is sent fire-and-forget, so a transport failure does not fail the request. For an immediate hard cut, compose this with the revoke-sessions endpoint or set the password outright. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_forceResetIdentityPassword","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"A password-reset email is sent so the identity can choose a new password. The current password and existing sessions stay valid until the reset link is used (completing the reset then rotates the password and revokes sessions). For an immediate hard cut, use the revoke-sessions endpoint; to rotate the password outright, use set-password."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/password/reset","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Force a password reset for an identity (admin)","tags":["api-identities"]}},"/api/v1/identities/{id}/sessions/revoke":{"post":{"description":"Revokes all of an identity's sessions (server-to-server admin) by marking every active refresh token revoked, so the next refresh on any device returns `401` and the user must sign in again. In-flight access tokens remain valid until their short TTL lapses. Idempotent — revoking when no tokens are active is a no-op. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_revokeIdentitySessions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Every active refresh token for the identity is revoked, so the next refresh on any device returns 401 and the user must sign in again. In-flight access tokens keep working until their short TTL expires. Use as a security-event response (e.g. suspected compromise) from backend tooling."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/sessions/revoke","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Revoke all of an identity's sessions (admin)","tags":["api-identities"]}},"/api/v1/identities/{id}/mfa":{"get":{"description":"Returns an identity's MFA enrollment summary — enrolled factors and recovery-code state — as a server-to-server admin read. MFA factors are a single set per identity across the Account; access is gated by an Environment-scoped membership check. Secret material is redacted in the response. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityMfa","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Returns the identity's enrolled factors (TOTP and WebAuthn rows with redacted secrets), the current recovery-code generation count + remaining count, and the most-recent admin-reset timestamp.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityMfaResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/mfa","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get MFA enrollment summary for an identity","tags":["api-identities"]}},"/api/v1/identities/{id}/mfa/reset":{"post":{"description":"Force-resets an identity's MFA enrollment (server-to-server admin), clearing all enrolled factors across the Account so the identity must re-enroll on next sign-in. Gated by an Environment-scoped membership check and audited. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_resetIdentityMfa","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Hard-deletes every enrolled factor, marks the current recovery-code generation redeemed, deletes every trusted-device row, and stamps `mfa_admin_reset_at`. The identity must re-enroll on next login."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/mfa/reset","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Force-reset an identity's MFA — admin recovery action","tags":["api-identities"]}},"/api/v1/identities/{id}/auth-state":{"get":{"description":"Returns an identity's authentication state for backend decisioning in a single call: `is_active`, `email_verified` (+ timestamp), `password_set`, lockout flags (`locked`, `locked_until`, `failed_login_attempts`), and MFA enrollment (`mfa_enrolled`, `mfa_factor_count`, `mfa_admin_reset_at`). This is a read — no audit row is written. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityAuthState","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Returns a snapshot of the identity's authentication state — active, email-verified, password-set, lockout, and MFA-enrolled flags — so a backend can gate its own flows in one read.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityAuthStateResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/auth-state","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Read an identity's auth state for backend decisioning","tags":["api-identities"]}},"/api/v1/identities/{id}/verify-email":{"post":{"description":"Marks an identity's email verified directly (server-to-server admin), setting `email_verified` and `email_verified_at` without a confirmation-link round-trip — intended for users already proven in a legacy system. Idempotent: re-verifying an already-verified identity is a no-op that is not re-audited; first-time verification writes an `identity.email.admin_verified` row because it bypasses proof of ownership. Returns `204 No Content`; returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_markIdentityEmailVerified","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Marks the identity's email as verified without the user clicking a link — the migration path for users already verified in a legacy system. Idempotent. Loudly audited; to instead send a verification email, use the Direct-API resend-verification endpoint."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/verify-email","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Mark an identity's email verified (admin)","tags":["api-identities"]}},"/api/v1/identities/{id}/assignments":{"get":{"description":"Returns a paginated list of an identity's role assignments within the current Environment, each resolved to its role and the node it is held at. Supports the standard paging query parameters. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityAssignments","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AssignmentResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get identity's role assignments","tags":["api-identities"]}},"/api/v1/identities/{id}/permissions":{"get":{"description":"Returns the identity's effective Application-wide permission keys for the current Environment as a flat `items` array of strings, resolved across all of its role assignments. Use this to answer \"what can this identity do\" without replaying individual assignments. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityPermissions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Effective App-wide permissions returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get identity's effective permissions","tags":["api-identities"]}},"/api/v1/identities/{id}/grants":{"get":{"description":"Returns, for the current Environment, every permission the identity holds mapped to the hierarchy nodes its grants were made at — the nodes themselves, **not** expanded through their descendants. A grant already means \"this node and everything beneath it\", so a caller answers a node-scoped question by walking up from the node in question and looking for one of these roots, using a copy of the tree fetched once from `GET /api/v1/nodes` and shared across identities. Answers both scopes without a further call: the permission appearing at all is the Application-wide answer, and the walk is the node-scoped one. Scheduled assignments outside their effective window are excluded. Returns `404` when the identity has no membership in this Environment.","operationId":"ApiIdentitiesController_getIdentityGrants","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Grant map returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityGrantResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities","method":"GET"}}}}},"404":{"description":"Identity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identities/{id}/grants","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get where an identity holds each permission","tags":["api-identities"]}},"/api/v1/identity-invites":{"get":{"description":"Returns a paginated list of identity invites for the active Application, scoped to the current Environment. Each row carries the invite's `email`, `intent`, optional initial role/node assignment, and a computed `status` (`pending`, `accepted`, `expired`, or `revoked`). Use the query parameters below to page, filter by `status`, and search by email or name.","operationId":"ApiIdentityInvitesController_listInvites","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by invite status","schema":{"type":"string","enum":["pending","accepted","revoked","expired"]}},{"name":"environment_id","required":false,"in":"query","description":"Filter by target Environment. Pass the literal `none` to return only directory-only invites (environment_id is null).","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityInviteResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List invites","tags":["api-identity-invites"]},"post":{"description":"Creates a single identity invite in the active Environment and returns it along with a tokenized `accept_url`. The effective intent is derived server-side from existing identity state — a net-new email becomes `activate`, an existing identity without active access becomes `add_to_app`, and `password_reset` is honored only for an existing identity. An optional `role_id` + `node_id` pair (both or neither) seeds an initial role assignment applied at acceptance. Invites expire after 7 days; the email is sent unless `send_email: false`, in which case the caller delivers `accept_url` itself. A duplicate pending invite for the same recipient returns `409`.","operationId":"ApiIdentityInvitesController_createInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIdentityInviteDto"}}}},"responses":{"201":{"description":"Invite created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiIdentityInviteResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create an invite","tags":["api-identity-invites"]}},"/api/v1/identity-invites/summary":{"get":{"description":"Returns invite counts for the active Environment grouped by status: `total_invites`, `pending_count`, `accepted_count`, `expired_count`, and `revoked_count`, plus `pending_with_role_count` for pending invites that carry an initial role assignment. Counts span the entire Environment, not a single page.","operationId":"ApiIdentityInvitesController_getInvitesSummary","parameters":[],"responses":{"200":{"description":"Invite status counts returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityInvitesSummaryDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get invite summary","tags":["api-identity-invites"]}},"/api/v1/identity-invites/bulk-create":{"post":{"description":"Creates many identity invites in one call against the active Environment. This is a partial-success endpoint: each row is processed independently, so per-row business errors (including same-batch duplicates) fold into a `failures` array rather than aborting the batch. Returns `200` when every row succeeds and `207 Multi-Status` when any row fails, with successful rows carrying their own `accept_url`. Authorization is evaluated once up front. Supports an optional `Idempotency-Key` header for safe retries.","operationId":"ApiIdentityInvitesController_bulkCreateInvites","parameters":[{"name":"Idempotency-Key","in":"header","description":"Optional opaque string the server uses to deduplicate retried requests. When the same key is replayed within 24 hours, the original response (status + body) is returned without re-processing — eliminates duplicate writes from network blips between server commit and client receipt. Generate one per logical operation (UUIDv4 recommended). Max 255 characters.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateIdentityInvitesDto"}}}},"responses":{"200":{"description":"Per-row invite creation. Each row is processed independently — duplicates, validation errors, and OAuth-client misconfiguration fail individually without aborting the batch. Returns 200 on full success or 207 Multi-Status on mixed outcomes; the body is always { summary, results } so callers iterate the same way regardless. — all items succeeded","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/ApiIdentityInviteResponseDto"}}}}}}}}},"207":{"description":"Per-row invite creation. Each row is processed independently — duplicates, validation errors, and OAuth-client misconfiguration fail individually without aborting the batch. Returns 200 on full success or 207 Multi-Status on mixed outcomes; the body is always { summary, results } so callers iterate the same way regardless. — mixed outcomes (one or more items failed)","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"oneOf":[{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/ApiIdentityInviteResponseDto"}}},{"type":"object","required":["index","status","code","input","error"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["error"]},"code":{"type":"integer"},"input":{"type":"object"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","nullable":true,"description":"Machine-readable error code"},"message":{"type":"string","description":"English fallback message"},"details":{"description":"Optional validation error details"}}}}}]}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk-create invites","tags":["api-identity-invites"]}},"/api/v1/identity-invites/{id}/resend":{"post":{"description":"Resends the email for a `pending` invite in the active Environment, generating a fresh token, invalidating the previous one, and resetting the 7-day expiry. Returns the new tokenized `accept_url`. Subject to a 5-minute cooldown between resends. Returns `404` when the invite is not found and `400` when it is not in `pending` status or the cooldown has not elapsed.","operationId":"ApiIdentityInvitesController_resendInvite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Invite resent","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiIdentityInviteResendResponseDto"}}}}}},"400":{"description":"Invite is no longer pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invite is no longer pending","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites/{id}/resend","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"404":{"description":"Invite not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Invite not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites/{id}/resend","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Resend an invite","tags":["api-identity-invites"]}},"/api/v1/identity-invites/{id}":{"delete":{"description":"Revokes a `pending` invite in the active Environment, marking it `revoked` so its token can no longer be accepted. Returns `204 No Content` on success. Returns `404` when the invite is not found and `400` when it has already been accepted and can no longer be revoked.","operationId":"ApiIdentityInvitesController_revokeInvite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Invite revoked"},"400":{"description":"Invite is no longer pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invite is no longer pending","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites/{id}","method":"DELETE"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites","method":"GET"}}}}},"404":{"description":"Invite not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Invite not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/identity-invites/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Revoke an invite","tags":["api-identity-invites"]}},"/api/v1/nodes":{"post":{"description":"Creates a hierarchy node in the active Application's current Environment. Authorization is evaluated against `hierarchy.manage` on the parent node when `parent_node_id` is supplied, or Application-wide when creating the root. The node type and the parent/child relationship are validated against the Environment's hierarchy schema, only one root node is permitted per Environment (a second returns `409`), and the configured `max_depth` is enforced; a missing `slug` is derived from the name. Returns `201` with the created node and writes a `node.created` audit row.","operationId":"ApiNodesController_createNode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNodeDto"}}}},"responses":{"201":{"description":"Node created successfully","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NodeResponseDto"}}}}}},"400":{"description":"Invalid parent-child relationship","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid parent-child relationship","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"409":{"description":"Root node already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Root node already exists","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create a hierarchy node","tags":["api-nodes"]},"get":{"description":"Returns the caller's accessible hierarchy as a nested tree for the current Environment, each node annotated with `access_state`, `can_access`, and `can_manage`. `user` principals (dashboard) see the full tree; API-key principals see only the subtree their `access_mode`/scopes grant — `full_access` unlocks everything, while scoped keys see nodes reachable via `hierarchy.view`. The response also includes a `scope` block listing `accessible_node_ids`, `scope_root_node_ids`, and `entry_node_ids`.","operationId":"ApiNodesController_listNodes","parameters":[{"name":"If-None-Match","in":"header","description":"An `ETag` from a previous response. Answers `304 Not Modified` when the hierarchy has not changed since, so a client holding a cached tree can revalidate on a short interval without re-reading it.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Scoped hierarchy tree returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScopedHierarchyTreeResponseDto"}}}}}},"304":{"description":"The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List hierarchy (scoped tree)","tags":["api-nodes"]}},"/api/v1/nodes/parents":{"get":{"description":"Returns the same hierarchy `GET /api/v1/nodes` describes, scoped identically, but as a flat list of `{ id, parent_node_id }` and nothing else. Intended for a client that evaluates authorization locally: it walks upward from a node and reads none of the tree's names, statuses, access flags or counts. At fifty thousand nodes the tree is roughly 18.6 MB against 4 MB of edges, on a read that runs at every consuming process's startup. Supports the same `If-None-Match` revalidation, with a validator distinct from the tree's — a tag from one representation never satisfies a request for the other.","operationId":"ApiNodesController_listNodeParents","parameters":[{"name":"If-None-Match","in":"header","description":"An `ETag` from a previous response. Answers `304 Not Modified` when the hierarchy has not changed since, so a client holding a cached tree can revalidate on a short interval without re-reading it.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Parent edges returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NodeParentEdgeResponseDto"}}}}}}},"304":{"description":"The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List the hierarchy as parent edges","tags":["api-nodes"]}},"/api/v1/nodes/{id}":{"get":{"description":"Returns a single hierarchy node in the current Environment along with the caller's access annotations for it — `access_state`, `can_access`, and `can_manage`. Returns `404` when the node id is outside the caller's accessible scope or does not exist in this Environment.","operationId":"ApiNodesController_getNode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Node returned with access state","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NodeAccessResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get a hierarchy node","tags":["api-nodes"]},"patch":{"description":"Updates a hierarchy node's mutable fields (`name`, `description`, `slug`, `status`, `metadata`, `parent_node_id`) in the current Environment, gated by `hierarchy.manage` on the node; reparenting is re-validated against the schema. Supply an `If-Match` header with the node's current `version` for optimistic concurrency — a stale value returns `409` with the current node state. Returns the updated node and writes a `node.updated` (or `node.moved`) audit row.","operationId":"ApiNodesController_updateNode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNodeDto"}}}},"responses":{"200":{"description":"Node updated successfully","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NodeResponseDto"}}}}}},"400":{"description":"Invalid parent-child relationship","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid parent-child relationship","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"PATCH"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"PATCH"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update a hierarchy node","tags":["api-nodes"]},"delete":{"description":"Soft-deletes a hierarchy node and its entire subtree in the current Environment by setting each node's status to `deleted` (rows are retained rather than hard-deleted), and removes every role assignment anchored at any node in the subtree so hidden branches can't keep granting access. Gated by `hierarchy.manage` on the node; the root node cannot be deleted (`400`). Supply an `If-Match` header with the node's current `version` for optimistic concurrency — a stale value returns `409`. Returns `204 No Content` and writes a `node.deleted` audit row.","operationId":"ApiNodesController_deleteNode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"responses":{"204":{"description":"Node deleted successfully"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"DELETE"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Delete a hierarchy node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/tree":{"get":{"description":"Returns the subtree rooted at the given node as a nested structure, drawn from the caller's accessible hierarchy in the current Environment. The node must be within the caller's accessible scope (otherwise `403`); a node id that resolves but is absent from the scoped tree returns `404`. Each node carries the same access annotations as the full tree listing.","operationId":"ApiNodesController_getSubtree","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Subtree returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/HierarchyTreeNodeDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get subtree from a node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/ancestors":{"get":{"description":"Returns the ancestor chain for a node as an `items` array ordered root-first, including the node itself, scoped to the current Environment. The node must be within the caller's accessible scope (otherwise `403`); a cross-Environment or unknown id returns `404`.","operationId":"ApiNodesController_getAncestors","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Ancestors returned root-first","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NodeResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get ancestor chain for a node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/children":{"get":{"description":"Returns the direct children of a node as an `items` array (one level only, not the full subtree), scoped to the current Environment. The node must be within the caller's accessible scope, otherwise `403` is returned.","operationId":"ApiNodesController_getChildren","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Children returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NodeResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get direct children of a node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/move":{"post":{"description":"Reparents a node under a new `parent_node_id` in the current Environment, gated by `hierarchy.manage` on the node; the move is re-validated against the schema (allowed parent/child types, depth, no cycles). Supply an `If-Match` header with the node's current `version` for optimistic concurrency — a stale value returns `409`. Returns the updated node and writes a `node.moved` audit row.","operationId":"ApiNodesController_moveNode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveNodeDto"}}}},"responses":{"200":{"description":"Node moved successfully","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NodeResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}/move","method":"POST"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}/move","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Move (reparent) a node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/identities":{"get":{"description":"Returns a paginated list of identity role assignments effective at a node, gated by `hierarchy.view` on that node. The result spans the node's full lineage so inherited assignments (granted at ancestor nodes) appear alongside direct ones, with each row flagged accordingly. Supports filtering by `q`, `role_id`, `access_type`, `status`, and `source_node_id`, plus sorting and paging. Returns `404` when the node is not found in this Environment.","operationId":"ApiNodesController_listIdentityAssignments","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Sort field","schema":{"default":"name","type":"string","enum":["name","role","access_type","created_at"]}},{"name":"order","required":false,"in":"query","description":"Sort order","schema":{"default":"asc","type":"string","enum":["asc","desc"]}},{"name":"role_id","required":false,"in":"query","description":"Filter by role ID","schema":{"type":"string"}},{"name":"access_type","required":false,"in":"query","description":"Filter by access type","schema":{"type":"string","enum":["direct","inherited"]}},{"name":"status","required":false,"in":"query","description":"Filter by assignment status","schema":{"type":"string","enum":["active","scheduled","expired"]}},{"name":"source_node_id","required":false,"in":"query","description":"Filter by source node ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityAssignmentRowDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}/identities","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List identities at a node","tags":["api-nodes"]}},"/api/v1/nodes/{id}/identities/summary":{"get":{"description":"Returns aggregate identity-assignment counts for a node, gated by `hierarchy.view`. Computed across the node's lineage so both direct and inherited assignments are reflected: `direct_count`, `inherited_count`, status breakdown (`active_count`, `scheduled_count`, `expired_count`, `expiring_soon_count`), `distinct_role_count`, a sample of the five most recent identities, and `can_manage_assignments` (whether the caller also holds `hierarchy.manage` here). Returns `404` when the node is not found in this Environment.","operationId":"ApiNodesController_getIdentityAssignmentsSummary","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Identity counts by status returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/NodeIdentitiesSummaryDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes","method":"GET"}}}}},"404":{"description":"Node not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Node not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/nodes/{id}/identities/summary","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get identity summary for a node","tags":["api-nodes"]}},"/api/v1/roles":{"get":{"description":"Returns the roles defined for the active Application within the current Environment as an unpaginated `{ items }` collection, each enriched with a `permission_count`. By default only active roles are returned; pass `include_inactive=true` to include deactivated roles, and filter by `type=system` or `type=custom`. Requires the `rbac.view_roles` permission.","operationId":"ApiRolesController_listRoles","parameters":[{"name":"include_inactive","required":false,"in":"query","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Roles returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoleResponseDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List roles","tags":["api-roles"]},"post":{"description":"Creates a custom role in the active Application for the current Environment from a `name` and optional `description`. The new role is always non-system and starts with no permissions — attach permission keys via the role-permissions endpoint. A name that already exists in the Environment returns `409`. Requires the `rbac.manage_roles` permission and emits a `role.created` audit event.","operationId":"ApiRolesController_createRole","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleDto"}}}},"responses":{"201":{"description":"Role created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RoleResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"409":{"description":"Role name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Role name already exists","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create a custom role","tags":["api-roles"]}},"/api/v1/roles/{id}":{"get":{"description":"Returns a single role by `id`, scoped to the active Application and current Environment. Returns `404` when no role with that id exists in the Environment. Requires the `rbac.view_roles` permission.","operationId":"ApiRolesController_getRole","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Role returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RoleResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get a role","tags":["api-roles"]},"patch":{"description":"Updates a role's `name`, `description`, and/or `is_active` flag by `id`. A name that collides with another role in the Environment returns `409`, and a missing role returns `404`. Supports optimistic concurrency via the `If-Match` header carrying the role's current version (`409` on a version mismatch). Requires the `rbac.manage_roles` permission.","operationId":"ApiRolesController_updateRole","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleDto"}}}},"responses":{"200":{"description":"Role updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RoleResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"PATCH"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update a role","tags":["api-roles"]},"delete":{"description":"Deletes a custom role by `id`; dependent role-assignment and role-permission rows are removed by database cascade. System roles are protected and return `403`, and a missing role returns `404`. Supports optimistic concurrency via the `If-Match` header (`409` on a version mismatch). Responds `204 No Content` and requires the `rbac.manage_roles` permission.","operationId":"ApiRolesController_deleteRole","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock version of the resource. Send the `version` field returned on read; the server rejects with 409 if the row has changed since.","required":false,"schema":{"type":"string"}}],"responses":{"204":{"description":"Role deleted"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"System role cannot be deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"System role cannot be deleted","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"DELETE"}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"DELETE"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Delete a role","tags":["api-roles"]}},"/api/v1/roles/{id}/permissions":{"get":{"description":"Returns the permission keys currently attached to a role as a sorted `{ items }` array of strings. Returns `404` when the role does not exist in the current Environment. Requires the `rbac.view_roles` permission.","operationId":"ApiRolesController_getRolePermissions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Permission keys returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get permissions for a role","tags":["api-roles"]},"put":{"description":"Replaces the role's entire permission set with the supplied `permission_keys` (a full overwrite, not a merge). Every key must exist in the Environment's catalog or the call returns `404`, and `system`-source permissions cannot be attached to a custom role (`400`). Returns `200` with a `{ message }` confirmation, requires the `rbac.manage_roles` permission, and emits a `role.permissions_updated` audit event.","operationId":"ApiRolesController_setRolePermissions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetRolePermissionsDto"}}}},"responses":{"200":{"description":"Permissions updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles","method":"GET"}}}}},"404":{"description":"Role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/roles/{id}/permissions","method":"PUT"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Replace permissions on a role","tags":["api-roles"]}},"/api/v1/assignments/app-wide":{"get":{"description":"Returns a paginated list of every direct role assignment across the active Application, scoped to the current Environment. Each row joins the assigned identity, the role, and the source hierarchy node, and carries a computed `status` of `active`, `scheduled`, or `expired` derived from the assignment's `effective_from`/`effective_to` window. Requires the `rbac.view_assignments` permission; supports `q` search plus filtering by `role_id`, `status`, and `source_node_id`, and sorting by `name`, `role`, or `created_at`.","operationId":"ApiAssignmentsController_listAppWideAssignments","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Sort field","schema":{"default":"name","type":"string","enum":["name","role","created_at"]}},{"name":"order","required":false,"in":"query","description":"Sort order","schema":{"default":"asc","type":"string","enum":["asc","desc"]}},{"name":"role_id","required":false,"in":"query","description":"Filter by role ID","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by assignment status","schema":{"type":"string","enum":["active","scheduled","expired"]}},{"name":"source_node_id","required":false,"in":"query","description":"Filter by source node ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityAssignmentRowDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List all assignments across the Application","tags":["api-assignments"]}},"/api/v1/assignments/app-wide/summary":{"get":{"description":"Returns aggregate assignment counts for the active Application within the current Environment: `active_count`, `scheduled_count`, `expired_count`, `expiring_soon_count`, and `distinct_role_count`. Counts cover all direct assignments in the Environment, not just the current page, and are intended for summary stat cards. Requires the `rbac.view_assignments` permission.","operationId":"ApiAssignmentsController_getAppWideAssignmentsSummary","parameters":[],"responses":{"200":{"description":"Assignment status counts returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AssignmentsSummaryDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get App-wide assignment summary","tags":["api-assignments"]}},"/api/v1/assignments":{"post":{"description":"Assigns a role to an identity at a specific hierarchy node, granting that role's permissions at the node and — through inheritance — at every descendant node. Authorization is evaluated as `rbac.manage_assignments` at the target `node_id`; the node must belong to the current Environment, the identity must have an active membership in it, and the role must be active and non-system (`400` for system roles). Optional `effective_from`/`effective_to` schedule the assignment; a duplicate identity + role + node triple returns `409`. Emits an `assignment.created` audit event.","operationId":"ApiAssignmentsController_assignRole","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAssignmentDto"}}}},"responses":{"201":{"description":"Assignment created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AssignmentResponseDto"}}}}}},"400":{"description":"System roles cannot be assigned to identities — they are reserved for platform administration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"System roles cannot be assigned to identities — they are reserved for platform administration","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"409":{"description":"Identity already has this role at this node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Identity already has this role at this node","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Assign a role to an identity at a node","tags":["api-assignments"]}},"/api/v1/assignments/{id}":{"patch":{"description":"Updates an existing assignment's role and/or its `effective_from`/`effective_to` window, identified by assignment `id`. Pass `null` for a date field to clear it, or omit the field to leave it unchanged; the new role must be active and non-system (`400` otherwise). Returns `404` when the assignment does not exist in the current Environment. Re-evaluates `rbac.manage_assignments` at the assignment's node before applying changes.","operationId":"ApiAssignmentsController_updateAssignment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssignmentDto"}}}},"responses":{"200":{"description":"Assignment updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AssignmentResponseDto"}}}}}},"400":{"description":"System roles cannot be assigned to identities — they are reserved for platform administration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"System roles cannot be assigned to identities — they are reserved for platform administration","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/{id}","method":"PATCH"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Assignment not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update an assignment","tags":["api-assignments"]},"delete":{"description":"Removes a single role assignment by `id`, revoking the role's permissions at its node and all inherited descendants. Authorization is re-evaluated as `rbac.manage_assignments` at the assignment's node; a missing assignment returns `404`. Responds `204 No Content` on success and emits an `assignment.removed` audit event carrying the affected identity, node, and role so cache-invalidating consumers can evict the right identity.","operationId":"ApiAssignmentsController_removeAssignment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Assignment removed"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Assignment not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Remove an assignment","tags":["api-assignments"]}},"/api/v1/assignments/bulk-remove":{"post":{"description":"Removes multiple assignments in a single atomic transaction given an array of `assignment_ids`. Every id is resolved and individually authorized as `rbac.manage_assignments` at its node before any deletion occurs — if any id is missing or unauthorized the whole batch fails and nothing is removed (all-or-nothing). Responds `204 No Content` and emits one `assignment.bulk_removed` audit event listing the affected identities, nodes, and roles.","operationId":"ApiAssignmentsController_bulkRemove","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRemoveAssignmentsDto"}}}},"responses":{"204":{"description":"Assignments removed"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk remove assignments","tags":["api-assignments"]}},"/api/v1/assignments/bulk-change-role":{"post":{"description":"Reassigns a batch of existing assignments (`assignment_ids`) to a single new `role_id` in one atomic transaction. The target role must be active and non-system (`400` otherwise); each assignment is authorized as `rbac.manage_assignments` at its node, and a change that would collide with an existing identity + role + node triple returns `409`. All-or-nothing: any failure aborts the batch. Returns `200` with a `{ message }` confirmation.","operationId":"ApiAssignmentsController_bulkChangeRole","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkChangeRoleDto"}}}},"responses":{"200":{"description":"Roles updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"System roles cannot be assigned to identities — they are reserved for platform administration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"System roles cannot be assigned to identities — they are reserved for platform administration","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/bulk-change-role","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk change role on assignments","tags":["api-assignments"]}},"/api/v1/assignments/bulk-create":{"post":{"description":"Creates many role assignments in one request using partial-success semantics: each item is validated, authorized at its node, and saved independently. Successes and failures are returned together in a `{ summary, results }` envelope — `200` when every item succeeds, `207 Multi-Status` when some fail. Per-item duplicates (including repeats within the same batch), unauthorized nodes, and inactive/system roles fold into the `failures` array rather than aborting the request. Emits a single `assignment.bulk_created` audit summary.","operationId":"ApiAssignmentsController_bulkCreate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateAssignmentsDto"}}}},"responses":{"200":{"description":"Assignments created — all items succeeded","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/AssignmentResponseDto"}}}}}}}}},"207":{"description":"Assignments created — mixed outcomes (one or more items failed)","content":{"application/json":{"schema":{"type":"object","required":["summary","results"],"properties":{"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer","description":"Total items in the request"},"succeeded":{"type":"integer","description":"Items that succeeded"},"failed":{"type":"integer","description":"Items that failed"}}},"results":{"type":"array","items":{"oneOf":[{"type":"object","required":["index","status","code","data"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["success"]},"code":{"type":"integer"},"data":{"$ref":"#/components/schemas/AssignmentResponseDto"}}},{"type":"object","required":["index","status","code","input","error"],"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["error"]},"code":{"type":"integer"},"input":{"type":"object"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","nullable":true,"description":"Machine-readable error code"},"message":{"type":"string","description":"English fallback message"},"details":{"description":"Optional validation error details"}}}}}]}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Bulk-create role assignments. All-or-nothing: any failure rolls back the entire batch","tags":["api-assignments"]}},"/api/v1/audit-events":{"get":{"description":"Cursor-paginated query over the Application's audit events, restricted to the identity surface — admin operational events never surface through the public API even when the API key holds every scope. Results are pinned to the principal's bound Application and Environment, so an env-pinned key cannot read identity rows from other Apps or Environments in the same Account. Supports the same filter DSL as the portal identity surface (action, category, severity, outcome, actor, resource, `correlation_id`, and full-text `q`). Each successful query writes one `audit.query.executed` row with `surface: \"api\"`; a malformed `cursor` returns `400`.","operationId":"ApiAuditEventsController_query","parameters":[{"name":"from","required":false,"in":"query","description":"Lower bound (inclusive). Defaults to `to - 30 days` when omitted.","schema":{"format":"date-time","type":"string"}},{"name":"to","required":false,"in":"query","description":"Upper bound (inclusive). Defaults to `now()` when omitted.","schema":{"format":"date-time","type":"string"}},{"name":"action","required":false,"in":"query","description":"Filter by one or more action keys. Repeat the query param (`?action=a&action=b`) or pass a comma-separated string.","schema":{"type":"array","items":{"type":"string"}}},{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["auth","identity","admin_user","tenancy","hierarchy","rbac","authorization","api_keys","oauth","webhooks","invites","mfa","audit","billing","sso","unknown"]}},{"name":"severity","required":false,"in":"query","schema":{"type":"string","enum":["info","notice","warning","critical"]}},{"name":"outcome","required":false,"in":"query","schema":{"type":"string","enum":["success","failure","denied"]}},{"name":"actor_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"actor_type","required":false,"in":"query","description":"Free-form actor-type filter (e.g. `user`, `identity`).","schema":{"type":"string"}},{"name":"resource_type","required":false,"in":"query","description":"Free-form resource-type filter (e.g. `identity`, `role`, `node`).","schema":{"type":"string"}},{"name":"resource_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"correlation_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"application_id","required":false,"in":"query","description":"Narrow to rows for one Application within the Account. Omit to include all Applications.","schema":{"format":"uuid","type":"string"}},{"name":"environment_id","required":false,"in":"query","description":"Narrow to rows for one Environment. Implies the Environment's parent Application. Omit to include all Environments.","schema":{"format":"uuid","type":"string"}},{"name":"q","required":false,"in":"query","description":"Full-text query against actor_label, resource_label (trigram), and metadata (GIN).","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque base64url cursor returned by the previous response. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size. Defaults to 50; max 200.","schema":{"minimum":1,"maximum":200,"default":50,"type":"number"}}],"responses":{"200":{"description":"Cursor-paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogRowDto"}},"pagination":{"type":"object","required":["next_cursor"],"properties":{"next_cursor":{"type":"string","nullable":true,"description":"Opaque base64url cursor. Echo back as the `cursor` query parameter to fetch the next page. `null` means end of feed."}}}}}}}},"400":{"description":"Cursor is malformed (bad base64, bad JSON shape, or invalid timestamp/UUID). Drop the cursor and start a fresh query.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Cursor is malformed (bad base64, bad JSON shape, or invalid timestamp/UUID). Drop the cursor and start a fresh query.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Query identity audit events","tags":["api-audit-events"]}},"/api/v1/audit-events/export":{"post":{"description":"Enqueues an asynchronous export of the filtered window and returns the `pending` job immediately — rendering happens off-request in a background worker that writes the file to object storage. Accepts the same filter DSL as the query endpoint in the request body; `cursor` and `limit` do not apply. `format: csv` (default) folds metadata into a single JSON column for spreadsheet review; `format: ndjson` emits one JSON object per line for SIEM ingestion (Splunk, Datadog, Elastic). Customer-visible rows only, scoped to this surface; the same 30-day default window applies when `from`/`to` are omitted. The dashboard is notified in realtime when the job completes. Writes an `audit.export.requested` row (who asked) and, on completion, an `audit.export.executed` row recording format, filter, and row count — never the row contents.","operationId":"ApiAuditEventsController_createExport","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditExportQueryDto"}}}},"responses":{"202":{"description":"Enqueues an asynchronous export of the filtered window and returns the `pending` job immediately — rendering happens off-request in a background worker that writes the file to object storage. Accepts the same filter DSL as the query endpoint in the request body; `cursor` and `limit` do not apply. `format: csv` (default) folds metadata into a single JSON column for spreadsheet review; `format: ndjson` emits one JSON object per line for SIEM ingestion (Splunk, Datadog, Elastic). Customer-visible rows only, scoped to this surface; the same 30-day default window applies when `from`/`to` are omitted. The dashboard is notified in realtime when the job completes. Writes an `audit.export.requested` row (who asked) and, on completion, an `audit.export.executed` row recording format, filter, and row count — never the row contents.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ExportJobDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create an identity audit-events export job (CSV / NDJSON)","tags":["api-audit-events"]},"get":{"description":"Returns the Account's most recent export jobs on this surface, newest first, with their current status. Completed jobs are retrieved individually to obtain a fresh download URL.","operationId":"ApiAuditEventsController_listExports","parameters":[],"responses":{"200":{"description":"Returns the Account's most recent export jobs on this surface, newest first, with their current status. Completed jobs are retrieved individually to obtain a fresh download URL.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExportJobDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List recent audit-log export jobs","tags":["api-audit-events"]}},"/api/v1/audit-events/export/{id}":{"get":{"description":"Returns one export job. When the job has completed, the response includes a short-lived (15-minute) signed download URL minted fresh on each read — the URL is never persisted, so a swept object cannot hand out a working link.","operationId":"ApiAuditEventsController_getExport","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Returns one export job. When the job has completed, the response includes a short-lived (15-minute) signed download URL minted fresh on each read — the URL is never persisted, so a swept object cannot hand out a working link.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ExportJobDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"404":{"description":"Export job not found — nonexistent, owned by another Account, or taken from a different surface (indistinguishable on the wire).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Export job not found — nonexistent, owned by another Account, or taken from a different surface (indistinguishable on the wire).","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events/export/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get an audit-log export job","tags":["api-audit-events"]},"delete":{"description":"Deletes the rendered file from object storage (if any) and the job record. Idempotent against an already-swept object.","operationId":"ApiAuditEventsController_deleteExport","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Deletes the rendered file from object storage (if any) and the job record. Idempotent against an already-swept object."},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"404":{"description":"Export job not found — nonexistent, owned by another Account, or taken from a different surface (indistinguishable on the wire).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Export job not found — nonexistent, owned by another Account, or taken from a different surface (indistinguishable on the wire).","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events/export/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Delete an audit-log export job","tags":["api-audit-events"]}},"/api/v1/audit-events/{id}":{"get":{"description":"Returns a single identity-surface audit event by id, plus two related-event sidecars: `related_by_correlation` (other rows sharing this row's `correlation_id`) and `related_by_actor` (the same actor's recent events). The lookup is pinned to the principal's bound Application and Environment, so an env-pinned key cannot fetch rows from outside its Environment. Reading a row writes one `audit.row.viewed` audit entry. Returns `404` for a missing row, an admin-only row, or a row outside the bound surface/Environment — the cases are indistinguishable on the wire to avoid leaking row ids.","operationId":"ApiAuditEventsController_getById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Public-API single-row endpoint. Restricted to identity-surface rows; returns 404 for admin-only rows even when the row id is correct.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuditLogDetailResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events","method":"GET"}}}}},"404":{"description":"Audit-log row not found. Indistinguishable from a row that exists but belongs to a different Account, a different surface, or is internal-only — disclosing the difference would leak the presence of cross-surface or platform-support rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Audit-log row not found. Indistinguishable from a row that exists but belongs to a different Account, a different surface, or is internal-only — disclosing the difference would leak the presence of cross-surface or platform-support rows.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/audit-events/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get an identity audit event with related events","tags":["api-audit-events"]}},"/api/v1/changes":{"get":{"description":"Forward, cursor-paginated catch-up feed of permission- and identity-relevant changes (role assignments, roles, permissions, hierarchy, membership, identity lifecycle, session revocation) — the pull complement to webhooks. Events are returned oldest-first as `{ id, event, data }`, byte-identical to the webhook delivery body so the same handler can process pushed and replayed events. Store `pagination.next_cursor` and pass it back as `?cursor=` to resume; an empty page echoes your cursor so you can keep polling. Account scope comes from the API-key / JWT principal; an env-pinned key only ever sees its own Application/Environment. Replay reaches only as far back as audit rows are retained.","operationId":"ApiChangesController_query","parameters":[{"name":"cursor","required":false,"in":"query","description":"Opaque base64url cursor returned by the previous response as `pagination.next_cursor`. Omit for the first poll.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size. Defaults to 50; max 200.","schema":{"minimum":1,"maximum":200,"default":50,"type":"number"}}],"responses":{"200":{"description":"Cursor-paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ChangeEventDto"}},"pagination":{"type":"object","required":["next_cursor"],"properties":{"next_cursor":{"type":"string","nullable":true,"description":"Opaque base64url cursor. Echo back as the `cursor` query parameter to fetch the next page. `null` means end of feed."}}}}}}}},"400":{"description":"Cursor is malformed (bad base64, bad JSON shape, or invalid timestamp/UUID). Drop the cursor and start from the beginning of the retained window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Cursor is malformed (bad base64, bad JSON shape, or invalid timestamp/UUID). Drop the cursor and start from the beginning of the retained window.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/changes","method":"GET"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/changes","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/changes","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Poll the permission-relevant change feed","tags":["api-changes"]}},"/api/v1/hierarchy-schema":{"get":{"description":"Returns the hierarchy schema for the active Environment, resolved from the principal context — the Application and Environment ride on the API key (env-pinned at issue time) or identity token, so the path carries no slugs. The schema defines the Environment's `node_types`, the `allowed_children` map governing parent/child rules, `max_depth`, and the `root_node_type` used when the root node is auto-created. Sibling Environments in the same Application can carry independent schemas; returns `null` when the Environment is in flat mode.","operationId":"ApiHierarchySchemaController_getSchema","parameters":[],"responses":{"200":{"description":"Hierarchy schema returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/HierarchySchemaResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/hierarchy-schema","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/hierarchy-schema","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get hierarchy schema for the active Environment","tags":["api-hierarchy-schema"]},"patch":{"description":"Replaces the active Environment's hierarchy schema wholesale with the supplied `node_types`, `allowed_children` map, `max_depth` (1–16), and `root_node_type` (which must be one of `node_types`). The update targets the Environment resolved from the principal context. Pass the Environment's current `version` in the `If-Match` header for optimistic locking — a stale value returns `409`. The persisted schema is re-read and returned in the response.","operationId":"ApiHierarchySchemaController_updateSchema","parameters":[{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}},{"name":"If-Match","in":"header","description":"Optimistic-lock token. Pass the `version` you read from the Environment — request fails 409 when the row has moved on.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHierarchySchemaDto"}}}},"responses":{"200":{"description":"Hierarchy schema updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/HierarchySchemaResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/hierarchy-schema","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/hierarchy-schema","method":"GET"}}}}},"409":{"description":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Version mismatch — the resource was modified since the version supplied in If-Match. Refresh and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/hierarchy-schema","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update hierarchy schema for the active Environment","tags":["api-hierarchy-schema"]}},"/api/v1/webhooks":{"post":{"description":"Creates a webhook subscription for the active Application's current Environment from a `url`, an `event_types` array, and an optional `description`. Use `[\"*\"]` to subscribe to all events (current and future); the wildcard cannot be mixed with concrete types, and any unsupported event type returns `400`. The response includes a freshly minted HMAC signing `secret` that is shown only once at creation — store it to verify delivery signatures. Requires the `webhook.manage` permission.","operationId":"ApiWebhooksController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookDto"}}}},"responses":{"201":{"description":"Webhook created — secret is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookCreatedResponseDto"}}}}}},"400":{"description":"One or more event types are not supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"One or more event types are not supported","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"POST"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create a webhook subscription","tags":["api-webhooks"]},"get":{"description":"Returns a paginated list of webhook subscriptions for the active Application's current Environment. Subscriptions are returned with metadata only — the HMAC signing secret is never included after creation. Supports `q` search on the target `url` plus paging and sorting; requires the `webhook.view` permission.","operationId":"ApiWebhooksController_list","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List webhook subscriptions","tags":["api-webhooks"]}},"/api/v1/webhooks/event-types":{"get":{"description":"Returns the catalog of event types that environment-scoped webhooks can subscribe to, as a static `{ items }` collection. Each entry describes an event type available for the active Application's Environment subscriptions; use these values (or `[\"*\"]`) when creating or updating a subscription. Requires no special RBAC permission beyond authentication.","operationId":"ApiWebhooksController_listEventTypes","parameters":[],"responses":{"200":{"description":"Supported webhook event types returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventTypeDto"}}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List subscribable webhook event types","tags":["api-webhooks"]}},"/api/v1/webhooks/{id}/deliveries":{"get":{"description":"Returns a paginated, newest-first history of delivery attempts for one webhook subscription (`id`), scoped to the active Application's current Environment. Each delivery includes its `event_type`, `status`, `response_status`, `response_body`, `attempts` count, and `next_retry_at`. Returns `404` when the subscription does not belong to this Environment; requires the `webhook.view` permission.","operationId":"ApiWebhooksController_listDeliveries","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"404":{"description":"Webhook subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Webhook subscription not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks/{id}/deliveries","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List delivery attempts for a webhook","tags":["api-webhooks"]}},"/api/v1/webhooks/{id}":{"get":{"description":"Returns a single webhook subscription by `id`, scoped to the active Application's current Environment, with metadata only — the signing secret is never re-disclosed. Returns `404` when the subscription does not exist in this Environment. Requires the `webhook.view` permission.","operationId":"ApiWebhooksController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook subscription returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"404":{"description":"Webhook subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Webhook subscription not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks/{id}","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Get a webhook subscription","tags":["api-webhooks"]},"patch":{"description":"Updates a webhook subscription's `url`, `event_types`, `description`, and/or `is_active` flag by `id`; omitted fields are left unchanged. When `event_types` is provided it is re-validated against the Environment's registry, and an unsupported type returns `400` (wildcard rules apply). Returns `404` for a subscription outside this Environment and requires the `webhook.manage` permission; the signing secret is not affected or returned.","operationId":"ApiWebhooksController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookDto"}}}},"responses":{"200":{"description":"Webhook subscription updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookResponseDto"}}}}}},"400":{"description":"One or more event types are not supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"One or more event types are not supported","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks/{id}","method":"PATCH"}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"404":{"description":"Webhook subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Webhook subscription not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Update a webhook subscription","tags":["api-webhooks"]},"delete":{"description":"Deletes a webhook subscription by `id`, scoped to the active Application's current Environment, stopping all future deliveries. Returns `404` when the subscription does not exist in this Environment. Responds `204 No Content` and requires the `webhook.manage` permission.","operationId":"ApiWebhooksController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Webhook subscription deleted"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks","method":"GET"}}}}},"404":{"description":"Webhook subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Webhook subscription not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/webhooks/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Delete a webhook subscription","tags":["api-webhooks"]}},"/api/v1/api-keys":{"post":{"description":"Issues a new API key in the active Application, scoped to the current Environment. The full `cnpy_`-prefixed secret is returned exactly once in the `key` field of the response and is never recoverable afterward — only a masked `key_preview` and metadata persist. `access_mode: \"scoped\"` requires a non-empty `scopes` array and enforces it on every authorization check; `access_mode: \"full_access\"` forbids `scopes` and bypasses RBAC entirely within the Application. Pass `expires_at` (ISO 8601) for an expiring key or omit it for none. Supports `Idempotency-Key` for safe retries, emits an `api_key.created` event, and is rate-limited to 20 requests per minute per Application.","operationId":"ApiApiKeysController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyDto"}}}},"responses":{"201":{"description":"API key created — key value is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreatedResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Create a new API key","tags":["api-api-keys"],"x-canopy-errors":[{"code":"api_keys.name_conflict","http_status":409},{"code":"api_keys.invalid_scope","http_status":400}],"x-canopy-rate-limit":{"requests":20,"window":"1m","scope":"Per Application"},"x-canopy-idempotent":true,"x-canopy-emits-events":["api_key.created"]},"get":{"description":"Returns a paginated list of every API key issued for the active Application, scoped to the current Environment. Keys are returned with metadata only — `key_preview`, `access_mode`, `scopes`, `is_active`, `last_used_at`, and `expires_at` — the secret value is shown once at creation and never again. Use the query parameters below to page, search keys by `name`, and sort by `name` or `created` (default: newest first).","operationId":"ApiApiKeysController_list","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"take","required":false,"in":"query","description":"Items per page (1-100, default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"q","required":false,"in":"query","description":"Search term","schema":{"maxLength":200,"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Column to sort by. Allowed values depend on the endpoint.","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyResponseDto"}},"pagination":{"$ref":"#/components/schemas/PageMetaDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"List API keys for Application","tags":["api-api-keys"]}},"/api/v1/api-keys/{id}":{"patch":{"description":"Enables or disables an existing API key in the current Environment via the `is_active` flag. Disabling is reversible: a disabled key is rejected by authentication immediately and can be re-enabled later. Returns the key's metadata with no secret. Returns `404` when no key with the given id exists in the active Environment.","operationId":"ApiApiKeysController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKeyDto"}}}},"responses":{"200":{"description":"API key updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiKeyResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"API key not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Activate or deactivate an API key","tags":["api-api-keys"]},"delete":{"description":"Permanently deletes an API key from the active Environment. The row is removed entirely rather than just disabled, so the key can never authenticate again and disappears from listings; the lifecycle remains traceable in the audit log. Returns `204 No Content` on success and `404` when no key with the given id exists in the active Environment.","operationId":"ApiApiKeysController_delete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"API key deleted"},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"API key not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Delete an API key","tags":["api-api-keys"]}},"/api/v1/api-keys/{id}/rotate-secret":{"post":{"description":"Rotates the secret of an existing API key in place, minting a brand-new `cnpy_`-prefixed value for the same row while preserving its id, name, `scopes`, `access_mode`, expiry, and audit lineage. The previous secret is invalidated immediately and the new plaintext is returned exactly once in the `key` field. `is_active` is left untouched — rotating does not re-enable a disabled key. Returns `404` when no key with the given id exists in the active Environment.","operationId":"ApiApiKeysController_rotateSecret","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"New key value returned once; the previous value stops working immediately","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreatedResponseDto"}}}}}},"401":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired token","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys","method":"GET"}}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"API key not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/api/v1/api-keys/{id}/rotate-secret","method":"POST"}}}}}},"security":[{"JWT":[]},{"API-Key":[]}],"summary":"Rotate an API key's secret","tags":["api-api-keys"]}}},"info":{"title":"Canopy Public API","description":"Public API endpoints for machine-to-machine integrations. Authenticate with an API key via the X-API-Key header, or a JWT Bearer token.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"API-Key":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key for management-tier access"},"JWT":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"JWT access token"}},"schemas":{"PermissionResponseDto":{"type":"object","properties":{"id":{"type":"string"},"application_id":{"type":"string"},"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"source":{"type":"string","enum":["system","custom"]},"created_at":{"format":"date-time","type":"string"},"version":{"type":"number","description":"Optimistic-lock version. Send back as the `If-Match` header when updating to detect concurrent edits."}},"required":["id","application_id","key","name","source","created_at","version"]},"PermissionItemDto":{"type":"object","properties":{"key":{"type":"string","description":"Unique permission key in resource.action format"},"name":{"type":"string","description":"Human-readable name (e.g. 'View identities')"},"description":{"type":"string","description":"Human-readable description"},"category":{"type":"string","description":"Category for grouping in the dashboard"}},"required":["key","name","category"]},"CreatePermissionsDto":{"type":"object","properties":{"permissions":{"description":"One or more permissions to register","type":"array","items":{"$ref":"#/components/schemas/PermissionItemDto"}}},"required":["permissions"]},"PermissionUsageRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_system_role":{"type":"boolean"},"is_active":{"type":"boolean","description":"Deactivated roles still carry the grant, so they are listed too — deleting the permission strips it from them as well."},"member_count":{"type":"number","description":"Number of distinct identities assigned this role, across all nodes."}},"required":["id","name","is_system_role","is_active","member_count"]},"PermissionUsageDto":{"type":"object","properties":{"permission_id":{"type":"string"},"role_count":{"type":"number","description":"Number of roles that grant this permission."},"identity_count":{"type":"number","description":"Distinct identities that hold this permission through any granting role. Lower than the sum of `member_count` when an identity holds more than one granting role."},"roles":{"type":"array","items":{"$ref":"#/components/schemas/PermissionUsageRoleDto"}}},"required":["permission_id","role_count","identity_count","roles"]},"UpdatePermissionDto":{"type":"object","properties":{"name":{"type":"string","description":"Updated name"},"description":{"type":"string","description":"Updated description","nullable":true},"category":{"type":"string","description":"Updated category"}}},"EvaluateResponseDto":{"type":"object","properties":{"allowed":{"type":"boolean"},"permission":{"type":"string"},"scope_evaluated":{"type":"string","enum":["node","app_wide"],"description":"Echo of the `scope` the caller asked for. `app_wide` answers are 'matched somewhere in the tree' — never treat them as scoped to a specific node."},"effective_node_id":{"type":"string","nullable":true,"description":"The hierarchy node the decision was anchored to. Always set for `scope_evaluated: \"node\"`; always null for `scope_evaluated: \"app_wide\"` because the answer is org-scoped, not node-scoped."},"granting_roles":{"type":"array","items":{"type":"string"}},"denial_reason":{"type":"string","nullable":true}},"required":["allowed","permission","scope_evaluated","granting_roles"]},"EvaluatePermissionDto":{"type":"object","properties":{"identity_id":{"type":"string","description":"Identity ID (from the `identities` table — the end user being evaluated, not an admin)."},"permission":{"type":"string"},"scope":{"type":"string","enum":["node","app_wide"],"description":"Required. `node` asks 'does this identity have the permission *at* `node_id`?' (lineage walk). `app_wide` asks the coarse-grained 'does this identity have the permission *anywhere* in the org?' question — useful for UI gating, **never** for resource-scoped enforcement. `node` requires `node_id`; `app_wide` forbids it."},"node_id":{"type":"string","description":"Required when `scope` is `node`; must be omitted when `scope` is `app_wide`."}},"required":["identity_id","permission","scope"]},"EvaluateCheckDto":{"type":"object","properties":{"identity_id":{"type":"string","description":"Identity ID (from the `identities` table — the end user being evaluated, not an admin)."},"permission":{"type":"string"},"scope":{"type":"string","enum":["node","app_wide"],"description":"Required. `node` asks 'does this identity have the permission *at* `node_id`?' (lineage walk). `app_wide` asks the coarse-grained 'does this identity have the permission *anywhere* in the org?' question — useful for UI gating, **never** for resource-scoped enforcement. `node` requires `node_id`; `app_wide` forbids it."},"node_id":{"type":"string","description":"Required when `scope` is `node`; must be omitted when `scope` is `app_wide`."}},"required":["identity_id","permission","scope"]},"BulkEvaluatePermissionDto":{"type":"object","properties":{"checks":{"type":"array","items":{"$ref":"#/components/schemas/EvaluateCheckDto"}}},"required":["checks"]},"PermissionTraceAssignmentDto":{"type":"object","properties":{"assignment_id":{"type":"string"},"role_id":{"type":"string"},"role_name":{"type":"string"},"active":{"type":"boolean","description":"Whether the assignment is within its effective_from/effective_to window right now."},"effective_from":{"type":"string","nullable":true},"effective_to":{"type":"string","nullable":true},"grants_permission":{"type":"boolean","description":"Whether the assigned role carries the explained permission."}},"required":["assignment_id","role_id","role_name","active","grants_permission"]},"PermissionTraceNodeDto":{"type":"object","properties":{"node_id":{"type":"string"},"name":{"type":"string"},"node_type":{"type":"string"},"depth":{"type":"number","description":"Index in the root-first lineage ordering (root = 0)."},"is_target":{"type":"boolean","description":"True for the node the explain was requested at (the deepest)."},"assignments":{"type":"array","items":{"$ref":"#/components/schemas/PermissionTraceAssignmentDto"}}},"required":["node_id","name","node_type","depth","is_target","assignments"]},"PermissionTraceDto":{"type":"object","properties":{"identity_id":{"type":"string"},"permission":{"type":"string"},"node_id":{"type":"string"},"allowed":{"type":"boolean"},"denial_reason":{"type":"string","nullable":true},"effective_node_id":{"type":"string","nullable":true,"description":"The hierarchy node the decision was anchored to."},"granting_role_ids":{"type":"array","items":{"type":"string"}},"granting_node_ids":{"description":"Lineage node IDs that carry a granting assignment — the inheritance path to highlight in the tree. Empty on a denial.","type":"array","items":{"type":"string"}},"lineage":{"description":"Root-first lineage of the target node, each with the identity's assignments observed at that node.","type":"array","items":{"$ref":"#/components/schemas/PermissionTraceNodeDto"}}},"required":["identity_id","permission","node_id","allowed","granting_role_ids","granting_node_ids","lineage"]},"ExplainPermissionDto":{"type":"object","properties":{"identity_id":{"type":"string","description":"Identity ID (from the `identities` table — the end user being explained, not an admin)."},"permission":{"type":"string"},"node_id":{"type":"string","description":"The hierarchy node to explain the decision at. The lineage from the root to this node is walked and returned in the trace."}},"required":["identity_id","permission","node_id"]},"PageMetaDto":{"type":"object","properties":{"page":{"type":"number","description":"Current page number (1-based)"},"take":{"type":"number","description":"Items per page"},"item_count":{"type":"number","description":"Total number of items matching the query"},"page_count":{"type":"number","description":"Total number of pages"},"has_previous_page":{"type":"boolean","description":"Whether a previous page exists"},"has_next_page":{"type":"boolean","description":"Whether a next page exists"}},"required":["page","take","item_count","page_count","has_previous_page","has_next_page"]},"IdentityResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"external_id":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","is_active","created_at"]},"IdentityRoleDto":{"type":"object","properties":{"assignment_id":{"type":"string","description":"ID of the user_role_assignments row connecting this identity, role, and node — needed to target this specific assignment for update/remove"},"id":{"type":"string"},"name":{"type":"string"},"node_id":{"type":"string"},"node_name":{"type":"string"},"effective_from":{"type":"string","nullable":true,"description":"When this assignment starts; null means it is already active"},"effective_to":{"type":"string","nullable":true,"description":"When this assignment expires; null means it never does"}},"required":["assignment_id","id","name","node_id","node_name","effective_from","effective_to"]},"IdentityRowDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/IdentityRoleDto"}},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","name","is_active","roles","created_at"]},"IdentitiesSummaryDto":{"type":"object","properties":{"total_identities":{"type":"number"},"assigned_count":{"type":"number"},"unassigned_count":{"type":"number"}},"required":["total_identities","assigned_count","unassigned_count"]},"CreateIdentityDto":{"type":"object","properties":{"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"password":{"type":"string","description":"Initial password (8-64 chars). NIST SP 800-63B aligned — no composition rules. HaveIBeenPwned breach check runs server-side. Omit to create a passwordless identity (sign-in via SSO/social or forgot-password reset)."},"external_id":{"type":"string"},"metadata":{"type":"object"},"role_id":{"type":"string"},"node_id":{"type":"string"}},"required":["email","first_name","last_name"]},"BulkCreateIdentitiesDto":{"type":"object","properties":{"identities":{"type":"array","items":{"$ref":"#/components/schemas/CreateIdentityDto"}}},"required":["identities"]},"IdentityDetailResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"external_id":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"is_active":{"type":"boolean"},"email_verified":{"type":"boolean"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/IdentityRoleDto"}},"can_manage":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","name","is_active","email_verified","roles","can_manage","created_at","updated_at"]},"MessageResponseDto":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"UpdateIdentityDto":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"is_active":{"type":"boolean"},"metadata":{"type":"object","nullable":true}}},"SetIdentityPasswordDto":{"type":"object","properties":{"password":{"type":"string","description":"New password (8–64 chars). NIST SP 800-63B aligned — no composition rules. HaveIBeenPwned breach check runs server-side.","minLength":8,"maxLength":64}},"required":["password"]},"MfaFactorResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["totp","webauthn"]},"label":{"type":"string","example":"iPhone 15"},"enrolled_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time","nullable":true,"description":"Last time this factor satisfied a challenge. `null` until the first use."}},"required":["id","type","label","enrolled_at","last_used_at"]},"AccountIdentityMfaResponseDto":{"type":"object","properties":{"factors":{"type":"array","items":{"$ref":"#/components/schemas/MfaFactorResponseDto"}},"recovery_codes_generation":{"type":"number","description":"Generation number of the active recovery-code batch. 0 means no batch has ever been issued; 1+ tracks regenerations."},"recovery_codes_remaining":{"type":"number","description":"Unredeemed code count in the current generation."},"mfa_admin_reset_at":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of the most recent admin force-reset. `null` if MFA has never been reset for this identity."}},"required":["factors","recovery_codes_generation","recovery_codes_remaining"]},"IdentityAuthStateResponseDto":{"type":"object","properties":{"is_active":{"type":"boolean","description":"Master kill-switch — false means the identity cannot sign in."},"email_verified":{"type":"boolean"},"email_verified_at":{"type":"string","format":"date-time","nullable":true},"password_set":{"type":"boolean","description":"Whether the identity has a usable password. False for passwordless (SSO/social/code-only) identities."},"locked":{"type":"boolean","description":"True while the identity is in an active failed-login lockout window."},"locked_until":{"type":"string","format":"date-time","nullable":true,"description":"When the current lockout lifts. `null` when not locked."},"failed_login_attempts":{"type":"number","description":"Consecutive failed login attempts on record."},"mfa_enrolled":{"type":"boolean","description":"Whether the identity has ≥ 1 enrolled MFA factor."},"mfa_factor_count":{"type":"number"},"mfa_admin_reset_at":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of the most recent admin MFA force-reset. `null` if never reset."}},"required":["is_active","email_verified","password_set","locked","failed_login_attempts","mfa_enrolled","mfa_factor_count"]},"AssignmentResponseDto":{"type":"object","properties":{"id":{"type":"string"},"identity_id":{"type":"string"},"application_node_id":{"type":"string"},"role_id":{"type":"string"},"effective_from":{"format":"date-time","type":"string","nullable":true},"effective_to":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","identity_id","application_node_id","role_id","created_at","updated_at"]},"IdentityGrantResponseDto":{"type":"object","properties":{"permission":{"type":"string","example":"reports.view"},"nodes":{"description":"Hierarchy node ids the permission was granted at. Not expanded through descendants.","type":"array","items":{"type":"string"}}},"required":["permission","nodes"]},"IdentityInviteResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"intent":{"type":"string","enum":["activate","add_to_app","password_reset","onboard"],"description":"Final intent stamped on the invite at create time. `activate` is the auto-derived default for net-new identities; `add_to_app` is auto-derived when the email matches an existing account-level identity that has no membership in this App; `password_reset` is admin-explicit. `onboard` may appear on rows created before the rename."},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"role_id":{"type":"string","nullable":true},"node_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true,"description":"Environment the invite targets, or null for a directory-only (account-tier) invite created with the Environment field left empty."},"environment_name":{"type":"string","nullable":true,"description":"Display name of the target Environment, or null for a directory-only invite."},"has_initial_assignment":{"type":"boolean"},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"expires_at":{"format":"date-time","type":"string"},"invited_by":{"type":"string"},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","intent","first_name","last_name","name","has_initial_assignment","status","expires_at","invited_by","created_at"]},"IdentityInvitesSummaryDto":{"type":"object","properties":{"total_invites":{"type":"number"},"pending_count":{"type":"number"},"pending_with_role_count":{"type":"number","description":"Subset of `pending_count` whose invites carry a role_id (invite-and-assign flow). Drives the dashboard's 'Assign Role to Identity' onboarding-step pending state — once an invite-with-role is accepted, the backend auto-creates the assignment, so we can show the assignment step as pending in the meantime."},"accepted_count":{"type":"number"},"expired_count":{"type":"number"},"revoked_count":{"type":"number"}},"required":["total_invites","pending_count","pending_with_role_count","accepted_count","expired_count","revoked_count"]},"ApiIdentityInviteResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"intent":{"type":"string","enum":["activate","add_to_app","password_reset","onboard"],"description":"Final intent stamped on the invite at create time. `activate` is the auto-derived default for net-new identities; `add_to_app` is auto-derived when the email matches an existing account-level identity that has no membership in this App; `password_reset` is admin-explicit. `onboard` may appear on rows created before the rename."},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"role_id":{"type":"string","nullable":true},"node_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true,"description":"Environment the invite targets, or null for a directory-only (account-tier) invite created with the Environment field left empty."},"environment_name":{"type":"string","nullable":true,"description":"Display name of the target Environment, or null for a directory-only invite."},"has_initial_assignment":{"type":"boolean"},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"expires_at":{"format":"date-time","type":"string"},"invited_by":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"accept_url":{"type":"string","description":"Tokenized URL the invitee would land on. Returned so callers that pass send_email=false can deliver it themselves."}},"required":["id","email","intent","first_name","last_name","name","has_initial_assignment","status","expires_at","invited_by","created_at","accept_url"]},"CreateIdentityInviteDto":{"type":"object","properties":{"client_id":{"type":"string","description":"OAuth client ID — determines which app the invite links to. If omitted, uses Canopy hosted fallback."},"intent":{"type":"string","enum":["activate","password_reset","onboard"],"description":"Optional. `activate` (default) creates a net-new identity OR — if an identity with this email already exists in the Account but has no active membership in this App — auto-derives an `add_to_app` invite that adds them to this App without touching their existing password. `password_reset` is the explicit admin-driven credential-rotation flow for an existing identity; it cannot carry a role/node assignment. The legacy `onboard` value is accepted and treated as `activate`."},"email":{"type":"string"},"first_name":{"type":"string","description":"Required for `activate`. Ignored for `add_to_app` (the existing identity's name wins) and for `password_reset`."},"last_name":{"type":"string","description":"Required for `activate`. Ignored for `add_to_app` and `password_reset`."},"role_id":{"type":"string","description":"Role ID — required if node_id is provided"},"node_id":{"type":"string","description":"Node ID — required if role_id is provided"},"send_email":{"type":"boolean","description":"Whether Canopy should send the invite email. Set false to suppress delivery and handle it yourself — the API response includes accept_url with the tokenized link. Defaults to true.","default":true}},"required":["email"]},"BulkCreateIdentityInvitesDto":{"type":"object","properties":{"invites":{"type":"array","items":{"$ref":"#/components/schemas/CreateIdentityInviteDto"}}},"required":["invites"]},"ApiIdentityInviteResendResponseDto":{"type":"object","properties":{"message":{"type":"string"},"accept_url":{"type":"string","description":"Tokenized URL for the regenerated invite. Resend rotates the token, which invalidates any accept_url returned from the original create call. Self-delivery callers must replace the stored URL with this one."}},"required":["message","accept_url"]},"NodeResponseDto":{"type":"object","properties":{"id":{"type":"string"},"application_id":{"type":"string"},"parent_node_id":{"type":"string","nullable":true},"node_type":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Optional free-text description shown under the node name."},"slug":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"},"version":{"type":"number","description":"Optimistic-lock version. Send back as the `If-Match` header when updating, moving, or deleting to detect concurrent edits."}},"required":["id","application_id","node_type","name","slug","status","metadata","created_at","updated_at","version"]},"CreateNodeDto":{"type":"object","properties":{"parent_node_id":{"type":"string","description":"Parent node ID (null for root)"},"node_type":{"type":"string","description":"Node type (org-defined, e.g. 'department', 'team')"},"name":{"type":"string","description":"Display name for the node"},"description":{"type":"string","description":"Optional free-text description shown under the node name"},"slug":{"type":"string","description":"URL-friendly slug (auto-generated if omitted)"},"metadata":{"type":"object","description":"Arbitrary metadata"}},"required":["node_type","name"]},"HierarchyTreeNodeDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Optional free-text description shown under the node name."},"slug":{"type":"string"},"node_type":{"type":"string","description":"Org-defined node type"},"parent_node_id":{"type":"string","nullable":true},"status":{"type":"string"},"access_state":{"type":"string","enum":["accessible","context_only"]},"can_access":{"type":"boolean"},"can_manage":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/HierarchyTreeNodeDto"}},"version":{"type":"number","description":"Optimistic-lock version. Send back as the `If-Match` header when mutating this node to detect concurrent edits."},"direct_count":{"type":"number","description":"Distinct active identities directly assigned at this node (excludes inherited)."},"inherited_count":{"type":"number","description":"Distinct active identities inherited from this node's ancestors."}},"required":["id","name","description","slug","node_type","parent_node_id","status","access_state","can_access","can_manage","children","version","direct_count","inherited_count"]},"HierarchyScopeDto":{"type":"object","properties":{"accessible_node_ids":{"type":"array","items":{"type":"string"}},"scope_root_node_ids":{"type":"array","items":{"type":"string"}},"entry_node_ids":{"type":"array","items":{"type":"string"}}},"required":["accessible_node_ids","scope_root_node_ids","entry_node_ids"]},"ScopedHierarchyTreeResponseDto":{"type":"object","properties":{"tree":{"type":"array","items":{"$ref":"#/components/schemas/HierarchyTreeNodeDto"}},"scope":{"$ref":"#/components/schemas/HierarchyScopeDto"}},"required":["tree","scope"]},"NodeParentEdgeResponseDto":{"type":"object","properties":{"id":{"type":"string"},"parent_node_id":{"type":"string","nullable":true,"description":"Null for the root node, which has no parent."}},"required":["id"]},"NodeAccessResponseDto":{"type":"object","properties":{"id":{"type":"string"},"application_id":{"type":"string"},"parent_node_id":{"type":"string","nullable":true},"node_type":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Optional free-text description shown under the node name."},"slug":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"},"version":{"type":"number","description":"Optimistic-lock version. Send back as the `If-Match` header when updating, moving, or deleting to detect concurrent edits."},"access_state":{"type":"string","enum":["accessible","context_only"]},"can_access":{"type":"boolean"},"can_manage":{"type":"boolean"}},"required":["id","application_id","node_type","name","slug","status","metadata","created_at","updated_at","version","access_state","can_access","can_manage"]},"UpdateNodeDto":{"type":"object","properties":{"name":{"type":"string","description":"Updated display name"},"description":{"type":"string","description":"Updated free-text description","nullable":true},"slug":{"type":"string","description":"Updated slug"},"status":{"type":"string","description":"Updated status (active/inactive)"},"metadata":{"type":"object","description":"Updated metadata"},"parent_node_id":{"type":"string","description":"New parent node ID (reparent)"}}},"MoveNodeDto":{"type":"object","properties":{"parent_node_id":{"type":"string"}},"required":["parent_node_id"]},"AssignmentIdentityDto":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"avatar_url":{"type":"string","nullable":true}},"required":["id","first_name","last_name","name","email"]},"AssignmentRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"AssignmentDetailDto":{"type":"object","properties":{"id":{"type":"string"},"source_node_id":{"type":"string"},"source_node_name":{"type":"string"},"access_type":{"type":"string","enum":["direct","inherited"]},"effective_from":{"format":"date-time","type":"string","nullable":true},"effective_to":{"format":"date-time","type":"string","nullable":true},"status":{"type":"string","enum":["active","scheduled","expired"]},"created_at":{"format":"date-time","type":"string"}},"required":["id","source_node_id","source_node_name","access_type","status","created_at"]},"IdentityAssignmentRowDto":{"type":"object","properties":{"identity":{"$ref":"#/components/schemas/AssignmentIdentityDto"},"role":{"$ref":"#/components/schemas/AssignmentRoleDto"},"assignment":{"$ref":"#/components/schemas/AssignmentDetailDto"}},"required":["identity","role","assignment"]},"RecentIdentityDto":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","nullable":true}},"required":["id","first_name","last_name","name"]},"NodeIdentitiesSummaryDto":{"type":"object","properties":{"direct_count":{"type":"number"},"inherited_count":{"type":"number"},"can_manage_assignments":{"type":"boolean"},"active_count":{"type":"number"},"scheduled_count":{"type":"number"},"expired_count":{"type":"number"},"expiring_soon_count":{"type":"number"},"distinct_role_count":{"type":"number"},"recent_identities":{"type":"array","items":{"$ref":"#/components/schemas/RecentIdentityDto"}}},"required":["direct_count","inherited_count","can_manage_assignments","active_count","scheduled_count","expired_count","expiring_soon_count","distinct_role_count","recent_identities"]},"RoleResponseDto":{"type":"object","properties":{"id":{"type":"string"},"application_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_system_role":{"type":"boolean"},"system_key":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"},"version":{"type":"number","description":"Optimistic-lock version. Send back as the `If-Match` header when updating to detect concurrent edits."},"permission_count":{"type":"number","description":"Number of permissions currently assigned to this role. Populated on list responses; omitted on single-role responses where the join isn't computed."},"member_count":{"type":"number","description":"Number of distinct identities assigned this role. Populated on list responses; omitted on single-role responses where the join isn't computed."}},"required":["id","application_id","name","is_system_role","is_active","created_at","updated_at","version"]},"CreateRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Role name"},"description":{"type":"string","description":"Role description"}},"required":["name"]},"UpdateRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Updated role name"},"description":{"type":"string","description":"Updated role description","nullable":true},"is_active":{"type":"boolean","description":"Whether the role is active"}}},"SetRolePermissionsDto":{"type":"object","properties":{"permission_keys":{"type":"array","items":{"type":"string"}}},"required":["permission_keys"]},"AssignmentsSummaryDto":{"type":"object","properties":{"active_count":{"type":"number"},"scheduled_count":{"type":"number"},"expired_count":{"type":"number"},"expiring_soon_count":{"type":"number"},"distinct_role_count":{"type":"number"}},"required":["active_count","scheduled_count","expired_count","expiring_soon_count","distinct_role_count"]},"CreateAssignmentDto":{"type":"object","properties":{"identity_id":{"type":"string"},"node_id":{"type":"string"},"role_id":{"type":"string"},"effective_from":{"type":"string"},"effective_to":{"type":"string"}},"required":["identity_id","node_id","role_id"]},"UpdateAssignmentDto":{"type":"object","properties":{"role_id":{"type":"string"},"effective_from":{"type":"string","nullable":true},"effective_to":{"type":"string","nullable":true}},"required":["role_id"]},"BulkRemoveAssignmentsDto":{"type":"object","properties":{"assignment_ids":{"type":"array","items":{"type":"string"}}},"required":["assignment_ids"]},"BulkChangeRoleDto":{"type":"object","properties":{"assignment_ids":{"type":"array","items":{"type":"string"}},"role_id":{"type":"string"}},"required":["assignment_ids","role_id"]},"BulkCreateAssignmentItemDto":{"type":"object","properties":{"identity_id":{"type":"string"},"node_id":{"type":"string"},"role_id":{"type":"string"},"effective_from":{"type":"string"},"effective_to":{"type":"string"}},"required":["identity_id","node_id","role_id"]},"BulkCreateAssignmentsDto":{"type":"object","properties":{"assignments":{"type":"array","items":{"$ref":"#/components/schemas/BulkCreateAssignmentItemDto"}}},"required":["assignments"]},"AuditLogRowDto":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string","nullable":true},"application_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true},"actor_id":{"type":"string","nullable":true},"actor_type":{"type":"string"},"action":{"type":"string"},"resource_type":{"type":"string","nullable":true},"resource_id":{"type":"string","nullable":true},"metadata":{"type":"object"},"created_at":{"format":"date-time","type":"string"},"actor_label":{"type":"string","nullable":true,"description":"Snapshot label of the actor at write time (email for users / identities, label otherwise). NULL on rows written before the Slice 2 deploy."},"resource_label":{"type":"string","nullable":true,"description":"Snapshot label of the affected resource at write time. NULL on rows written before the Slice 2 deploy."},"correlation_id":{"type":"string","nullable":true,"format":"uuid","description":"Per-request correlation ID — every row written during the same HTTP request (or background job) shares this value. Use the query API's `correlation_id` filter to reconstruct a chain."},"outcome":{"type":"string","enum":["success","failure","denied"],"description":"`success` for normal-flow events, `failure` for errors, `denied` for authorization rejections."},"category":{"type":"string","nullable":true,"enum":["auth","identity","admin_user","tenancy","hierarchy","rbac","authorization","api_keys","oauth","webhooks","invites","mfa","audit","billing","sso","unknown"],"description":"Broad classification from the action catalog (e.g. `auth`, `rbac`). NULL on legacy rows whose action key was unknown at backfill time — those carry the `unknown` value in storage."},"severity":{"type":"string","enum":["info","notice","warning","critical"],"description":"`info` for normal-flow events, `notice` for important state changes, `warning` for failures, `critical` for security events."},"customer_visible":{"type":"boolean","description":"Whether this row should appear in customer-facing audit views. `false` rows are internal-only (platform support)."},"identity_visible":{"type":"boolean","description":"Whether this row should appear on a future end-user `My security activity` surface. Not consumed by current reads."}},"required":["id","actor_type","action","metadata","created_at","outcome","severity","customer_visible","identity_visible"]},"ExportJobDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","processing","completed","failed"],"description":"`pending` (enqueued), `processing` (worker rendering), `completed` (file ready), or `failed` (see `error`)."},"surface":{"type":"string","enum":["admin","identities"],"description":"The audit surface the export was taken from."},"format":{"type":"string","enum":["csv","ndjson"],"description":"Wire format of the rendered file."},"application_id":{"type":"string","nullable":true,"format":"uuid"},"environment_id":{"type":"string","nullable":true,"format":"uuid"},"row_count":{"type":"number","nullable":true,"description":"Rows written, set once the job completes."},"error":{"type":"string","nullable":true,"description":"Failure detail, present only when `status` is `failed`."},"created_at":{"format":"date-time","type":"string"},"completed_at":{"format":"date-time","type":"string","nullable":true},"expires_at":{"format":"date-time","type":"string","nullable":true,"description":"When the stored file + this record become eligible for sweep."},"download_url":{"type":"string","nullable":true,"description":"Short-lived signed download URL. Present only on the single-job read of a completed job; `null` otherwise."}},"required":["id","status","surface","format","created_at"]},"AuditExportQueryDto":{"type":"object","properties":{"from":{"type":"string","format":"date-time","description":"Lower bound (inclusive). Defaults to `to - 30 days` when omitted."},"to":{"type":"string","format":"date-time","description":"Upper bound (inclusive). Defaults to `now()` when omitted."},"action":{"description":"Filter by one or more action keys. Repeat the query param (`?action=a&action=b`) or pass a comma-separated string.","type":"array","items":{"type":"string"}},"category":{"type":"string","enum":["auth","identity","admin_user","tenancy","hierarchy","rbac","authorization","api_keys","oauth","webhooks","invites","mfa","audit","billing","sso","unknown"]},"severity":{"type":"string","enum":["info","notice","warning","critical"]},"outcome":{"type":"string","enum":["success","failure","denied"]},"actor_id":{"type":"string","format":"uuid"},"actor_type":{"type":"string","description":"Free-form actor-type filter (e.g. `user`, `identity`)."},"resource_type":{"type":"string","description":"Free-form resource-type filter (e.g. `identity`, `role`, `node`)."},"resource_id":{"type":"string","format":"uuid"},"correlation_id":{"type":"string","format":"uuid"},"application_id":{"type":"string","format":"uuid","description":"Narrow to rows for one Application within the Account. Omit to include all Applications."},"environment_id":{"type":"string","format":"uuid","description":"Narrow to rows for one Environment. Implies the Environment's parent Application. Omit to include all Environments."},"q":{"type":"string","description":"Full-text query against actor_label, resource_label (trigram), and metadata (GIN)."},"format":{"type":"string","enum":["csv","ndjson"],"default":"csv","description":"Export wire format. `csv` for spreadsheet review (metadata folded into one JSON column); `ndjson` for SIEM ingestion (one JSON object per line, metadata nested). Defaults to `csv`."}}},"AuditLogDetailResponseDto":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string","nullable":true},"application_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true},"actor_id":{"type":"string","nullable":true},"actor_type":{"type":"string"},"action":{"type":"string"},"resource_type":{"type":"string","nullable":true},"resource_id":{"type":"string","nullable":true},"metadata":{"type":"object"},"created_at":{"format":"date-time","type":"string"},"actor_label":{"type":"string","nullable":true,"description":"Snapshot label of the actor at write time (email for users / identities, label otherwise). NULL on rows written before the Slice 2 deploy."},"resource_label":{"type":"string","nullable":true,"description":"Snapshot label of the affected resource at write time. NULL on rows written before the Slice 2 deploy."},"correlation_id":{"type":"string","nullable":true,"format":"uuid","description":"Per-request correlation ID — every row written during the same HTTP request (or background job) shares this value. Use the query API's `correlation_id` filter to reconstruct a chain."},"outcome":{"type":"string","enum":["success","failure","denied"],"description":"`success` for normal-flow events, `failure` for errors, `denied` for authorization rejections."},"category":{"type":"string","nullable":true,"enum":["auth","identity","admin_user","tenancy","hierarchy","rbac","authorization","api_keys","oauth","webhooks","invites","mfa","audit","billing","sso","unknown"],"description":"Broad classification from the action catalog (e.g. `auth`, `rbac`). NULL on legacy rows whose action key was unknown at backfill time — those carry the `unknown` value in storage."},"severity":{"type":"string","enum":["info","notice","warning","critical"],"description":"`info` for normal-flow events, `notice` for important state changes, `warning` for failures, `critical` for security events."},"customer_visible":{"type":"boolean","description":"Whether this row should appear in customer-facing audit views. `false` rows are internal-only (platform support)."},"identity_visible":{"type":"boolean","description":"Whether this row should appear on a future end-user `My security activity` surface. Not consumed by current reads."},"related_by_correlation":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogRowDto"}},"related_by_actor":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogRowDto"}}},"required":["id","actor_type","action","metadata","created_at","outcome","severity","customer_visible","identity_visible","related_by_correlation","related_by_actor"]},"ChangeEventDataDto":{"type":"object","properties":{"account_id":{"type":"string","nullable":true},"application_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true},"actor_id":{"type":"string","nullable":true},"actor_type":{"type":"string"},"resource_type":{"type":"string","nullable":true},"resource_id":{"type":"string","nullable":true},"metadata":{"type":"object"},"timestamp":{"type":"string","description":"ISO-8601 time the change occurred."}},"required":["actor_type","metadata","timestamp"]},"ChangeEventDto":{"type":"object","properties":{"id":{"type":"string","description":"Audit row id of the change. Also embedded in the page cursor; use it to de-duplicate against live webhook deliveries."},"event":{"type":"string","description":"The event type — identical to the webhook event/audit action (e.g. `assignment.removed`, `role.permissions.updated`)."},"data":{"$ref":"#/components/schemas/ChangeEventDataDto"}},"required":["id","event","data"]},"HierarchySchemaResponseDto":{"type":"object","properties":{"node_types":{"type":"array","items":{"type":"string"}},"allowed_children":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"max_depth":{"type":"number"},"root_node_type":{"type":"string","nullable":true}},"required":["node_types","allowed_children","max_depth"]},"UpdateHierarchySchemaDto":{"type":"object","properties":{"node_types":{"description":"Allowed node types (e.g. ['organization', 'region', 'team']). Order is informational, not structural — parent/child rules are governed by `allowed_children`.","type":"array","items":{"type":"string"}},"allowed_children":{"type":"object","description":"Map from node type to allowed child node types. Empty array means leaf-only.","example":{"organization":["region"],"region":["team"],"team":[]}},"max_depth":{"type":"number","description":"Maximum nesting depth (root counts as depth 1).","minimum":1,"maximum":16},"root_node_type":{"type":"string","description":"Node type used when the root node is auto-created. Must be one of `node_types`."}},"required":["node_types","allowed_children","max_depth","root_node_type"]},"WebhookCreatedResponseDto":{"type":"object","properties":{"id":{"type":"string"},"scope":{"type":"string","enum":["environment","account"]},"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"secret":{"type":"string","description":"HMAC secret — only shown once on creation"}},"required":["id","scope","url","event_types","is_active","created_at","secret"]},"CreateWebhookDto":{"type":"object","properties":{"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"description":{"type":"string"}},"required":["url","event_types"]},"WebhookResponseDto":{"type":"object","properties":{"id":{"type":"string"},"scope":{"type":"string","enum":["environment","account"]},"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"}},"required":["id","scope","url","event_types","is_active","created_at"]},"WebhookEventTypeDto":{"type":"object","properties":{"event_type":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"}},"required":["event_type","category","description"]},"WebhookDeliveryResponseDto":{"type":"object","properties":{"id":{"type":"string"},"event_type":{"type":"string"},"status":{"type":"string","enum":["pending","success","failed"]},"response_status":{"type":"number","nullable":true},"response_body":{"type":"string","nullable":true},"attempts":{"type":"number"},"next_retry_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["id","event_type","status","attempts","created_at"]},"UpdateWebhookDto":{"type":"object","properties":{"url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string"}},"description":{"type":"string","nullable":true},"is_active":{"type":"boolean"}}},"ApiKeyCreatedResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"key":{"type":"string","description":"Plaintext API key — shown only once"},"key_preview":{"type":"string"},"access_mode":{"type":"string","enum":["scoped","full_access"],"description":"`full_access` keys bypass RBAC entirely within the Application. `scoped` keys enforce the `scopes` array. Surface this prominently in any UI that lists keys — it's the difference between a routine integration credential and an App-wide bearer token."},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["id","name","key","key_preview","access_mode","scopes","created_at"]},"CreateApiKeyDto":{"type":"object","properties":{"name":{"type":"string","description":"API key name"},"description":{"type":"string","description":"API key description"},"access_mode":{"type":"string","enum":["scoped","full_access"],"description":"Required. `scoped` enforces the `scopes` array on every authorization check (deny if the requested permission isn't listed). `full_access` bypasses RBAC entirely within the key's Application — every permission is granted. Pick `scoped` whenever possible; `full_access` should be a deliberate choice (use cases: bootstrap automation, trusted backend services that legitimately need App-wide access). `scoped` requires a non-empty `scopes` array; `full_access` forbids `scopes`."},"scopes":{"description":"Permission scopes this key is authorized for. Required and must be non-empty when `access_mode` is `scoped`. Must be omitted when `access_mode` is `full_access`.","type":"array","items":{"type":"string"}},"expires_at":{"type":"string","description":"Expiration date (ISO 8601). Omit for no expiration."}},"required":["name","access_mode"]},"ApiKeyResponseDto":{"type":"object","properties":{"id":{"type":"string"},"client_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"key_preview":{"type":"string","description":"Masked preview of the raw key (first 12 chars + `****`)."},"access_mode":{"type":"string","enum":["scoped","full_access"],"description":"`full_access` keys bypass RBAC entirely within the Application. `scoped` keys enforce the `scopes` array."},"scopes":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"last_used_at":{"format":"date-time","type":"string","nullable":true},"expires_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["id","client_id","name","key_preview","access_mode","scopes","is_active","created_at"]},"UpdateApiKeyDto":{"type":"object","properties":{"is_active":{"type":"boolean","description":"Whether the key is active. Set `false` to deactivate (the key stops authenticating immediately, reversibly); set `true` to reactivate."}},"required":["is_active"]},"ApiErrorBodyDto":{"type":"object","properties":{"statusCode":{"type":"number","example":401,"description":"HTTP status code."},"code":{"type":"string","nullable":true,"example":"auth.invalid_token","description":"Stable, machine-readable error code (a `MSG.*` constant). `null` for generic framework errors that carry no domain code."},"message":{"type":"string","example":"Invalid or expired token","description":"Human-readable English fallback message."},"timestamp":{"type":"string","example":"2026-04-20T12:00:00.000Z","description":"ISO-8601 timestamp of when the error was produced."},"path":{"type":"string","example":"/api/v1/api-keys","description":"Request path that produced the error."},"method":{"type":"string","example":"GET","description":"Request HTTP method."},"details":{"description":"Field-level validation messages, present only on 400 responses from request-body validation.","example":["name must be a string"],"type":"array","items":{"type":"string"}}},"required":["statusCode","code","message","timestamp","path","method"]},"ErrorResponseDto":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/ApiErrorBodyDto"}},"required":["error"]}}}}