{"openapi":"3.0.0","paths":{"/v1/auth/register":{"post":{"description":"Public self-service signup for a platform admin. `POST` accepting `email`, `password`, `first_name`, `last_name`, `account_name`, and `application_name`; rate-limited to 5 per hour per IP. Always returns the same generic 201 — the outcome is delivered by email so the response never reveals whether the address is already registered or how it authenticates. For a brand-new email it parks a `PendingRegistration` and emails a 24-hour verification link — the real `User`, `Account`, owner `AccountMember`, and first `Application` are not created until the email is verified. If the email already belongs to a user the account-creation intent is stashed and the owner is emailed either a sign-in notice (password user) or a set-password link (SSO-only user); no duplicate is created, and the parked intent materializes only after the owner signs in and explicitly confirms the reviewed names. Passwords are checked against a breach list and argon2id-hashed on every path so response timing is uniform.","operationId":"AuthController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDto"}}}},"responses":{"201":{"description":"Generic success; the outcome is delivered to the email address","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Register a new user","tags":["auth"]}},"/v1/auth/accounts":{"post":{"description":"Creates an additional `Account` (plus a Free subscription, owner membership, first `Application`, and seeded Development/Production environments) for the already-authenticated admin without creating a new user; guarded by `UserJwtGuard` and rate-limited to 10 per hour. Returns `201` with an access token already bound to the new Account's first Application and sets the admin refresh and session-marker cookies, so the SPA lands directly in the new Account. An SSO-only admin with no password is refused with `403` — but the intent is first parked and a set-password link emailed, giving them the same set-password path as a logged-out SSO user.","operationId":"AuthController_createAccount","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountDto"}}}},"responses":{"201":{"description":"New Account created; a session bound to its first Application is returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CreateAccountResponseDto"}}}}}},"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":"/v1/auth/accounts","method":"POST"}}}}},"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":"/v1/auth/accounts","method":"POST"}}}}}},"summary":"Create an additional Account for the authenticated admin (no new user)","tags":["auth"]}},"/v1/auth/accounts/resume":{"get":{"description":"Returns the signed-in admin's parked account-creation intent — the `account_name` and `application_name` captured on the register form — so the SPA can show them for explicit confirmation before resuming. The stash is written pre-auth by whoever submitted the register form with this email, so the names are unconfirmed input and are never acted on without this review step. Guarded by `UserJwtGuard`. Returns `404` when nothing is parked or the intent has expired.","operationId":"AuthController_getPendingAccountCreation","parameters":[],"responses":{"200":{"description":"The parked account and application names, for explicit confirmation","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PendingAccountCreationResponseDto"}}}}}},"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":"/v1/auth/accounts/resume","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":"/v1/auth/accounts/resume","method":"GET"}}}}},"404":{"description":"No pending account creation to resume","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No pending account creation to resume","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/accounts/resume","method":"GET"}}}}}},"summary":"Review the parked account-creation intent before resuming","tags":["auth"]},"delete":{"description":"Discards the signed-in admin's parked account-creation intent without provisioning anything — the decline branch of the resume review, covering both \"I changed my mind\" and \"this wasn't me\" (the register form is public, so a third party can park an intent against any known email). Guarded by `UserJwtGuard`. Idempotent: returns `204` whether or not an intent existed.","operationId":"AuthController_dismissPendingAccountCreation","parameters":[],"responses":{"204":{"description":"Parked intent discarded"},"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":"/v1/auth/accounts/resume","method":"DELETE"}}}}},"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":"/v1/auth/accounts/resume","method":"DELETE"}}}}}},"summary":"Dismiss the parked account-creation intent without creating anything","tags":["auth"]},"post":{"description":"Completes a deferred account creation that was parked when an existing user submitted the register form while logged out, then signed in or reset their password. The parked names are unauthenticated register-form input, so the SPA first fetches them (`GET accounts/resume`) and calls this only after the signed-in owner explicitly confirms — never silently on sign-in. Guarded by `UserJwtGuard` and rate-limited to 10 per hour; the parked intent is claimed delete-first so a retried or concurrent call cannot double-create. Returns `201` with an access token bound to the new Account's first Application and sets the refresh and session-marker cookies, or `404` when there is no pending intent to resume.","operationId":"AuthController_resumeAccountCreation","parameters":[],"responses":{"201":{"description":"Parked Account created; a session bound to its first Application is returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CreateAccountResponseDto"}}}}}},"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":"/v1/auth/accounts/resume","method":"POST"}}}}},"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":"/v1/auth/accounts/resume","method":"POST"}}}}},"404":{"description":"No pending account creation to resume","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No pending account creation to resume","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/accounts/resume","method":"POST"}}}}}},"summary":"Resume a deferred account creation parked at the register form","tags":["auth"]}},"/v1/auth/invite-info":{"post":{"description":"Public lookup of a pending admin invite by its raw `token`, so the accept-invite page can prefill the form. `POST` (rate-limited to 10 per minute) returning the invitee's `email`, `first_name`, `last_name`, and an `sso_available` flag that is true when the Account has an active admin SSO connection covering the invitee's domain. Returns `400` when the token is unknown, expired, or the invite is no longer pending.","operationId":"AuthController_getInviteInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoRequestDto"}}}},"responses":{"200":{"description":"Invite details returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/InviteInfoResponseDto"}}}}}}},"summary":"Look up invite details by token","tags":["auth"]}},"/v1/auth/accept-invite":{"post":{"description":"Public endpoint that redeems an admin invite token and provisions the invitee as an active, email-verified `User` (or updates an existing unverified one), then adds the `AccountMember` row joining them to the inviting Account. `POST` accepting `token`, `first_name`, `last_name`, and an optional `password`; rate-limited to 5 per hour. A password is required and breach-checked unless the Account has active admin SSO for the invitee's domain, in which case password-less acceptance is allowed. Returns `400` for an invalid, expired, or already-consumed token, or a missing password without SSO.","operationId":"AuthController_acceptInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteDto"}}}},"responses":{"200":{"description":"Invite accepted, user created or updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Accept an invite","tags":["auth"]}},"/v1/auth/login":{"post":{"description":"Public password authentication for platform admins, rate-limited to 5 per 15 minutes per IP. On success returns either a fully-bound access token (single Account — landing on that Account's first Application) or, for multi-Account users, a pre-auth token plus an Accounts list requiring a follow-up `select-account` call; the refresh token rides an HttpOnly cookie alongside a session marker, and a pre-auth token sets its own cookie. After 5 failed attempts the user is locked out for escalating windows (15/30/45/60 minutes), and `401` is returned for bad credentials, a locked, deactivated, or unverified account; `403` for an admin with no Account membership. `access_token` is `Bearer`, TTL 900s.","operationId":"AuthController_login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"200":{"description":"Tokens returned (or Account selection required)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/LoginResponseDto"}}}}}}},"summary":"Authenticate and receive tokens","tags":["auth"]}},"/v1/auth/select-account":{"post":{"description":"Second step of multi-Account login: exchanges the pre-auth session for an access token bound to the chosen Account's first Application. Guarded by `UserJwtGuard` with `@AllowPreAuth`, accepting `account_id`; the caller must be an active member of that Account or `401` is returned. Issues a fresh access and refresh token (Bearer, 900s), sets the refresh and session-marker cookies, and clears the pre-auth cookie. There is no Application picker — switching Apps happens later via `switch-application`.","operationId":"AuthController_selectAccount","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelectAccountDto"}}}},"responses":{"200":{"description":"Tokens returned bound to the selected Account's first Application","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthTokenResponseDto"}}}}}},"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":"/v1/auth/select-account","method":"POST"}}}}},"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":"/v1/auth/select-account","method":"POST"}}}}}},"summary":"Select an Account from a pre-auth session (multi-Account login)","tags":["auth"]}},"/v1/auth/switch-application":{"post":{"description":"Re-issues tokens bound to a different `Application` within the caller's currently selected Account — the dashboard's App switcher — without re-authentication. Guarded by `UserJwtGuard`, accepting `application_id`; the target App must belong to the token's Account and the caller must be an active member, otherwise `401`. Cross-Account switching is refused (a fresh login is required), preventing privilege escalation. Returns a new access and refresh token (Bearer, 900s) and rotates the refresh and session-marker cookies.","operationId":"AuthController_switchApplication","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchApplicationDto"}}}},"responses":{"200":{"description":"Fresh access + refresh tokens issued for the new Application","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthTokenResponseDto"}}}}}},"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":"/v1/auth/switch-application","method":"POST"}}}}},"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":"/v1/auth/switch-application","method":"POST"}}}}}},"summary":"Switch the active Application in an authenticated session","tags":["auth"]}},"/v1/auth/refresh":{"post":{"description":"Rotates the session by reading the admin refresh token from its HttpOnly cookie (the body is ignored) and issuing a new access token plus a new refresh token, preserving the original Account and Application binding. Public but rate-limited to 60 per minute. The old refresh token is revoked on use; presenting an already-revoked token triggers reuse detection that revokes every refresh token for that user. Returns `401` when the cookie is missing, the token is unknown, expired, revoked, or the user or their Account membership is no longer active. New access token is Bearer with a 900s TTL.","operationId":"AuthController_refresh","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenDto"}}}},"responses":{"200":{"description":"New tokens returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthTokenResponseDto"}}}}}},"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":"/v1/auth/refresh","method":"POST"}}}}}},"summary":"Refresh access token","tags":["auth"]}},"/v1/auth/logout":{"post":{"description":"Revokes the current session and clears the refresh, pre-auth, and session-marker cookies. Guarded by `UserJwtGuard`; reads the refresh token from its cookie and accepts a `revoke_all` flag — when true, every active refresh token for the user is revoked (sign out everywhere) rather than just the current one. Returns the `revoked_count`.","operationId":"AuthController_logout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutDto"}}}},"responses":{"200":{"description":"Tokens revoked","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RevokeResponseDto"}}}}}},"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":"/v1/auth/logout","method":"POST"}}}}},"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":"/v1/auth/logout","method":"POST"}}}}}},"summary":"Logout and revoke tokens","tags":["auth"]}},"/v1/auth/verify-email":{"get":{"description":"Public `GET` that confirms a signup by its `token` query parameter, materializing the parked `PendingRegistration` into the real `User`, `Account`, Free subscription, owner `AccountMember`, first `Application`, and seeded environments in one transaction, then consuming the pending row so it cannot double-materialize. Returns `400` when the token is unknown, already consumed, or expired (links are valid for 24 hours).","operationId":"AuthController_verifyEmail","parameters":[{"name":"token","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Email verified","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Verify email address","tags":["auth"]}},"/v1/auth/resend-verification":{"post":{"description":"Public endpoint that re-sends the signup verification email for the given `email`, refreshing the pending registration's token and 24-hour expiry. Rate-limited to 3 per hour. Always responds with a generic success message regardless of whether a matching unverified registration exists, to avoid account enumeration.","operationId":"AuthController_resendVerification","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordDto"}}}},"responses":{"200":{"description":"Verification email sent","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Resend verification email","tags":["auth"]}},"/v1/auth/resend-verification-by-token":{"post":{"description":"Public variant of resend-verification keyed by the original (possibly expired) verification `token` rather than the email, so the hosted verify-email page can offer one-click resend without the address in the URL. Rate-limited to 3 per hour; the pending row survives an expired token, so an expired link can always be refreshed. Always returns a generic success message to avoid leaking whether the token maps to anything.","operationId":"AuthController_resendVerificationByToken","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationByTokenDto"}}}},"responses":{"200":{"description":"Verification email sent","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Resend verification email by token","tags":["auth"]}},"/v1/auth/forgot-password":{"post":{"description":"Public endpoint that issues a one-hour password-reset token for the given `email` and emails the reset link. Rate-limited to 5 per hour. Always responds with a generic success message whether or not a matching user exists, to prevent account enumeration.","operationId":"AuthController_forgotPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordDto"}}}},"responses":{"200":{"description":"Reset email sent (if account exists)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Request password reset","tags":["auth"]}},"/v1/auth/reset-password":{"post":{"description":"Public endpoint that completes a password reset using the emailed `token` and a new `password`. Rate-limited to 5 per hour; the token must be unredeemed and within its one-hour expiry or `400` is returned. The new password is breach-checked and rejected if it matches any of the last 5 passwords, then argon2id-hashed; on success the failed-attempt lockout is cleared and all of the user's refresh tokens are revoked, forcing re-login everywhere.","operationId":"AuthController_resetPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":"Password reset","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Reset password with token","tags":["auth"]}},"/v1/auth/change-password":{"post":{"description":"Authenticated password change requiring the `current_password` and a `new_password`. Guarded by `UserJwtGuard` and rate-limited to 5 per hour; returns `401` when the current password is wrong. The new password is breach-checked and rejected if it matches any of the last 5 passwords (`400`), then argon2id-hashed. Existing sessions are not revoked.","operationId":"AuthController_changePassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordDto"}}}},"responses":{"200":{"description":"Password changed","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":"/v1/auth/change-password","method":"POST"}}}}},"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":"/v1/auth/change-password","method":"POST"}}}}}},"summary":"Change password (authenticated)","tags":["auth"]}},"/v1/auth/me":{"get":{"description":"Returns the authenticated admin's profile — `id`, `email`, name, `avatar_url`, `email_verified`, `created_at`, and the caller's `account_role` (`owner`/`admin`/`member`, or null when the token is not Account-bound) so the dashboard can gate owner-only surfaces. Guarded by `UserJwtGuard`.","operationId":"AuthController_getProfile","parameters":[],"responses":{"200":{"description":"Profile returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthProfileDto"}}}}}},"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":"/v1/auth/me","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":"/v1/auth/me","method":"GET"}}}}}},"summary":"Get current user profile","tags":["auth"]}},"/v1/auth/me/avatar":{"post":{"operationId":"AuthController_uploadAvatar","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Avatar uploaded; updated profile returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthProfileDto"}}}}}},"400":{"description":"Invalid image (unsupported type or malformed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid image (unsupported type or malformed)","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/me/avatar","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":"/v1/auth/me/avatar","method":"POST"}}}}},"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":"/v1/auth/me/avatar","method":"POST"}}}}},"413":{"description":"Image exceeds the 5 MB size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":413,"code":null,"message":"Image exceeds the 5 MB size limit","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/me/avatar","method":"POST"}}}}}},"summary":"Upload profile avatar","tags":["auth"]},"delete":{"operationId":"AuthController_deleteAvatar","parameters":[],"responses":{"204":{"description":"Avatar 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":"/v1/auth/me/avatar","method":"DELETE"}}}}},"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":"/v1/auth/me/avatar","method":"DELETE"}}}}}},"summary":"Remove profile avatar","tags":["auth"]},"get":{"operationId":"AuthController_getAvatar","parameters":[],"responses":{"200":{"description":"Avatar image streamed","content":{"image/webp":{"schema":{"type":"string","format":"binary"}}}},"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":"/v1/auth/me/avatar","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":"/v1/auth/me/avatar","method":"GET"}}}}},"404":{"description":"No avatar set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No avatar set","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/me/avatar","method":"GET"}}}}}},"summary":"Get current user avatar image","tags":["auth"]}},"/v1/auth/me/email":{"post":{"operationId":"AuthController_requestEmailChange","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestEmailChangeDto"}}}},"responses":{"200":{"description":"Confirmation link sent to the new address","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"New email matches the current email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"New email matches the current email","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/me/email","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":"/v1/auth/me/email","method":"POST"}}}}},"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":"/v1/auth/me/email","method":"POST"}}}}},"409":{"description":"That email is already in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"That email is already in use","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/me/email","method":"POST"}}}}}},"summary":"Request an email-address change","tags":["auth"]}},"/v1/auth/confirm-email-change":{"post":{"operationId":"AuthController_confirmEmailChange","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmEmailChangeDto"}}}},"responses":{"200":{"description":"Email address updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired confirmation token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired confirmation token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/auth/confirm-email-change","method":"POST"}}}}}},"summary":"Confirm an email-address change","tags":["auth"]}},"/v1/auth/me/permissions":{"get":{"description":"Returns the authenticated principal's app-wide permission keys as an `items` array. Guarded by `UserJwtGuard` and rate-limited to 10 per minute. Admin principals (`type: user`) bypass RBAC entirely and always receive an empty array — they are recognized by principal type, not by permission list; an empty array is also returned when the token carries no Account. For non-admin principals the list is resolved from active (date-bounded) role assignments in the token's environment.","operationId":"AuthController_getMyPermissions","parameters":[],"responses":{"200":{"description":"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":"/v1/auth/me/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":"/v1/auth/me/permissions","method":"GET"}}}}}},"summary":"Get current user permissions","tags":["auth"]}},"/v1/auth/me/accounts":{"get":{"description":"Returns the distinct `Account`s the authenticated user belongs to, each with its Applications, as an `items` array — the data behind the multi-Account picker. Guarded by `UserJwtGuard`.","operationId":"AuthController_getAccounts","parameters":[],"responses":{"200":{"description":"Accounts returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AuthAccountDto"}}}}}}},"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":"/v1/auth/me/accounts","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":"/v1/auth/me/accounts","method":"GET"}}}}}},"summary":"List the Accounts the current user belongs to","tags":["auth"]}},"/v1/auth/sessions":{"get":{"description":"Lists the authenticated user's active sessions (non-revoked, unexpired refresh tokens), newest first, as an `items` array exposing each session's `id`, `ip_address`, `user_agent`, `created_at`, and `expires_at`. Guarded by `UserJwtGuard`.","operationId":"AuthController_listSessions","parameters":[],"responses":{"200":{"description":"Sessions returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SessionDto"}}}}}}},"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":"/v1/auth/sessions","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":"/v1/auth/sessions","method":"GET"}}}}}},"summary":"List active sessions","tags":["auth"]},"delete":{"description":"Revokes every active session for the authenticated user (sign out everywhere). Guarded by `UserJwtGuard`; returns `204 No Content`.","operationId":"AuthController_revokeAllSessions","parameters":[],"responses":{"204":{"description":"All sessions revoked"},"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":"/v1/auth/sessions","method":"DELETE"}}}}},"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":"/v1/auth/sessions","method":"DELETE"}}}}}},"summary":"Revoke all sessions","tags":["auth"]}},"/v1/auth/sessions/{id}":{"delete":{"description":"Revokes one of the caller's own sessions by its refresh-token `id`. Guarded by `UserJwtGuard`; returns `204 No Content` on success and `404` when the session does not exist or does not belong to the caller.","operationId":"AuthController_revokeSession","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Session revoked"},"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":"/v1/auth/sessions/{id}","method":"DELETE"}}}}},"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":"/v1/auth/sessions/{id}","method":"DELETE"}}}}}},"summary":"Revoke a specific session","tags":["auth"]}},"/v1/identity/auth/config":{"get":{"description":"Public, unauthenticated lookup that resolves an Environment from its `publishable_key` query param and returns its pre-login sign-in configuration: which methods are enabled (`password_login_enabled`, `email_otp_login_enabled`), whether self-signup is open (`allow_self_signup`), and the CAPTCHA requirements plus public Turnstile site key. Lets a bring-your-own-UI client render the right form before any credential is collected. Also the way to verify a publishable key during integration setup: an unrecognized key returns a distinct `400` here, deliberately unlike login, which answers the same generic `401` for a bad key as for bad credentials. Throttled to 60 requests/minute.","operationId":"IdentityAuthController_getConfig","parameters":[{"name":"publishable_key","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Public, pre-login CAPTCHA configuration (resolved by publishable key) so a bring-your-own-UI client knows whether to render the Turnstile widget and with which site key","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityAuthConfigDto"}}}}}},"400":{"description":"Invalid publishable key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid publishable key","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/config","method":"GET"}}}}}},"summary":"Get an Environment's public sign-in configuration by publishable key","tags":["identity-auth"]}},"/v1/identity/auth/login":{"post":{"description":"Authenticates an end-user identity with email + password against the Environment named by the `publishable_key` in the body, enforcing per-Environment method/CAPTCHA gates, lockout, active+verified+membership checks. On success returns a short-lived `access_token` (`Bearer`, ~15min) plus identity context; the refresh token rides in the httpOnly `ca_identity_refresh_token` cookie for browsers, or in the JSON body when a trusted backend presents a matching secret `X-API-Key` (BFF mode). When the Environment requires MFA it returns an `mfa_challenge`, `mfa_enrollment_pending`, or grace continuation instead of tokens. Rate-limited to 5 attempts per 15 minutes; `401` on invalid credentials, `403` on failed login CAPTCHA.","operationId":"IdentityAuthController_login","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLoginDto"}}}},"responses":{"200":{"description":"Tokens returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid credentials","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login","method":"POST"}}}}},"403":{"description":"CAPTCHA verification failed (Environment requires login CAPTCHA)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"CAPTCHA verification failed (Environment requires login CAPTCHA)","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login","method":"POST"}}}}}},"summary":"Authenticate identity and receive tokens","tags":["identity-auth"]}},"/v1/identity/auth/login/email-otp/start":{"post":{"description":"Begins passwordless email-OTP login: resolves the Environment from `publishable_key` and, only if the identity exists and is active+verified, emails a 6-digit code (sha256-hashed, 15-min TTL, attempt-limited). Always returns the same generic message regardless of whether the key or identity exists (anti-enumeration), and never auto-provisions. Rate-limited to 5 per 15 minutes; `403` when the email-OTP sign-in method is disabled for the Environment.","operationId":"IdentityAuthController_emailOtpLoginStart","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEmailOtpStartDto"}}}},"responses":{"200":{"description":"Generic response — a 6-digit login code is emailed only if the identity exists and is eligible (anti-enumeration)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"403":{"description":"The requested sign-in method is disabled for this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"The requested sign-in method is disabled for this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/email-otp/start","method":"POST"}}}}}},"summary":"Request a passwordless email login code","tags":["identity-auth"]}},"/v1/identity/auth/login/email-otp/verify":{"post":{"description":"Completes passwordless login by verifying the emailed 6-digit `code` for the given email against the Environment named by `publishable_key`, issuing tokens on success exactly like password login (short-lived `Bearer` access token; refresh via httpOnly cookie or body for a trusted backend `X-API-Key`). May instead return an MFA challenge when the Environment mandates step-up. Rate-limited to 10 per 10 minutes; generic `401` on an invalid/expired code, `403` when the method is disabled.","operationId":"IdentityAuthController_emailOtpLoginVerify","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEmailOtpVerifyDto"}}}},"responses":{"200":{"description":"Tokens returned (or an MFA challenge); generic 401 on an invalid/expired code","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Invalid or expired login code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired login code","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/email-otp/verify","method":"POST"}}}}},"403":{"description":"The requested sign-in method is disabled for this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"The requested sign-in method is disabled for this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/email-otp/verify","method":"POST"}}}}}},"summary":"Verify an email login code and receive tokens","tags":["identity-auth"]}},"/v1/identity/auth/login/mfa-enroll/totp/verify":{"post":{"description":"Completes a hard `enrollment_required` continuation from login by enrolling the identity's first TOTP factor. Public and sessionless — the sealed `enrollment_token` from the login response is the bearer, verified together with the 6-digit `code`. On success a fully MFA-bound session is minted (access token plus refresh via cookie/body) and one-time recovery codes are returned; this unblocks MFA-mandatory Environments with no grace window. Rate-limited to 10 per 10 minutes; `401` on an invalid or expired enrollment token.","operationId":"IdentityAuthController_enrollTotpDuringLogin","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLoginMfaEnrollTotpVerifyDto"}}}},"responses":{"200":{"description":"Tokens returned (plus one-time recovery codes on the first factor); 401 on an invalid token/factor","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Invalid or expired enrollment continuation token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired enrollment continuation token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/mfa-enroll/totp/verify","method":"POST"}}}}}},"summary":"Complete a first TOTP factor during login and receive tokens","tags":["identity-auth"]}},"/v1/identity/auth/login/mfa-enroll/webauthn/options":{"post":{"description":"Step 1 of enrolling a first WebAuthn factor during a login `enrollment_required` continuation: returns `PublicKeyCredentialCreationOptions` for `navigator.credentials.create()`. Public and sessionless — keyed by the sealed `enrollment_token` rather than an authenticated session. Rate-limited to 10 per 10 minutes; `401` on an invalid or expired enrollment token.","operationId":"IdentityAuthController_enrollWebAuthnOptionsDuringLogin","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLoginMfaEnrollWebAuthnOptionsDto"}}}},"responses":{"200":{"description":"Get WebAuthn attestation options to enroll a first factor during login","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaWebAuthnEnrollOptionsResponseDto"}}}}}},"401":{"description":"Invalid or expired enrollment continuation token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired enrollment continuation token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/mfa-enroll/webauthn/options","method":"POST"}}}}}},"summary":"Get WebAuthn attestation options to enroll a first factor during login","tags":["identity-auth"]}},"/v1/identity/auth/login/mfa-enroll/webauthn/verify":{"post":{"description":"Step 2 of first-factor WebAuthn enrollment during login: verifies the authenticator attestation against the challenge carried by the sealed `enrollment_token`, persists the WebAuthn factor, and mints a fully MFA-bound session (access token plus refresh via cookie/body) along with one-time recovery codes. Public and sessionless. Rate-limited to 10 per 10 minutes; `401` on an invalid or expired enrollment token.","operationId":"IdentityAuthController_enrollWebAuthnDuringLogin","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLoginMfaEnrollWebAuthnVerifyDto"}}}},"responses":{"200":{"description":"Tokens returned (plus one-time recovery codes on the first factor); 401 on an invalid token/factor","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Invalid or expired enrollment continuation token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid or expired enrollment continuation token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/login/mfa-enroll/webauthn/verify","method":"POST"}}}}}},"summary":"Complete a first WebAuthn factor during login and receive tokens","tags":["identity-auth"]}},"/v1/identity/auth/refresh":{"post":{"description":"Rotates an identity session: consumes the current refresh token (from the httpOnly `ca_identity_refresh_token` cookie for browsers, or the request body for a trusted backend) and returns a new short-lived `access_token` (`Bearer`, `expires_in` 900s) plus a freshly rotated refresh token, re-bound to the same Environment and re-checking active membership. The old token is revoked; replaying a revoked token triggers reuse-detection that revokes every session for the identity. Throttled to 60 per minute; `401` when the token is missing, expired, revoked, or the identity is no longer an active member.","operationId":"IdentityAuthController_refresh","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenDto"}}}},"responses":{"200":{"description":"New tokens returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityTokenResponseDto"}}}}}},"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":"/v1/identity/auth/refresh","method":"POST"}}}}}},"summary":"Refresh identity access token","tags":["identity-auth"]}},"/v1/identity/auth/switch-organization":{"post":{"description":"Re-issues the identity session for another organization the identity belongs to, rotating the refresh token and minting an access token whose `org_id`/`org_role` claims name the target. Takes the refresh credential the way `/refresh` does: browser callers rely on the httpOnly cookie, backend (BFF) callers replay `refresh_token` in the body. When the target organization's policy requires MFA and the session has not presented a factor, the response is `requires_mfa_challenge: true` with an `mfa_challenge` to complete; the session stays in its current organization until the challenge is verified. Returns `403` when the identity does not belong to the requested organization (`organization.not_a_member`) or must enroll a factor first (`organization.mfa_enrollment_required`), and `401` for an invalid, revoked, or expired session.","operationId":"IdentityAuthController_switchOrganization","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchOrganizationDto"}}}},"responses":{"200":{"description":"Session re-issued for the organization","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityOrganizationSwitchResponseDto"}}}}}},"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":"/v1/identity/auth/switch-organization","method":"POST"}}}}},"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":"/v1/identity/auth/switch-organization","method":"POST"}}}}}},"summary":"Switch the session's organization","tags":["identity-auth"]}},"/v1/identity/auth/organizations":{"get":{"description":"Lists the organizations the signed-in identity belongs to in this Environment, most recent membership first, each with the single role held there — the rows an org switcher renders. Empty when the Environment does not use the organizations access model. Requires a valid identity access token.","operationId":"IdentityAuthController_listOrganizations","parameters":[],"responses":{"200":{"description":"Organizations returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentityOrganizationResponseDto"}}}}}}},"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":"/v1/identity/auth/organizations","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":"/v1/identity/auth/organizations","method":"GET"}}}}}},"summary":"List the session's organizations","tags":["identity-auth"]}},"/v1/identity/auth/logout":{"post":{"description":"Revokes one identity session for the authenticated caller (guarded by `IdentityJwtGuard`): the refresh token named by the `ca_identity_refresh_token` cookie for browser callers, or by `refresh_token` in the body for a client that holds its own token (a backend proxy, or a native app whose key delivers it in the body). The cookie is cleared either way. With neither a cookie nor a body token, every session of the identity is revoked. The in-flight access token remains valid until its short TTL expires. Returns a generic confirmation message; `401`/`403` when the bearer is missing/invalid or is not an identity principal.","operationId":"IdentityAuthController_logout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLogoutDto"}}}},"responses":{"200":{"description":"Tokens revoked","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":"/v1/identity/auth/logout","method":"POST"}}}}},"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":"/v1/identity/auth/logout","method":"POST"}}}}}},"summary":"Logout identity and revoke tokens","tags":["identity-auth"]}},"/v1/identity/auth/verify-email":{"post":{"description":"Confirms an identity's email address from the single-use link `token` emailed at signup (sha256-hashed for lookup, with an expiry). Public and unauthenticated; already-verified tokens succeed idempotently. Rate-limited to 5 per hour; `400` on an invalid or expired token. This is the link-based counterpart to the OTP-code verification endpoint.","operationId":"IdentityAuthController_verifyEmail","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityVerifyEmailDto"}}}},"responses":{"200":{"description":"Email verified","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired verification token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired verification token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/verify-email","method":"POST"}}}}}},"summary":"Verify identity email address","tags":["identity-auth"]}},"/v1/identity/auth/verify-email-code":{"post":{"description":"Confirms an identity's email using the 6-digit OTP `code` emailed at self-signup, scoped to the email + Environment resolved from `publishable_key` and attempt-limited so the short code can't be brute-forced. Every failure mode (unknown email, already verified, expired, locked, wrong code) returns the same generic error to avoid an enumeration oracle. Rate-limited to 10 per 10 minutes; `400` on an invalid or expired code.","operationId":"IdentityAuthController_verifyEmailCode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityVerifyEmailCodeDto"}}}},"responses":{"200":{"description":"Email verified","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired verification code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired verification code","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/verify-email-code","method":"POST"}}}}}},"summary":"Verify identity email with a self-signup OTP code","tags":["identity-auth"]}},"/v1/identity/auth/resend-verification":{"post":{"description":"Re-sends the email-verification message for an identity, resolving the Account from `publishable_key` before dispatching to the supplied email. Public and unauthenticated. Heavily rate-limited to 3 per hour to prevent mailbox flooding; `401` on an invalid publishable key.","operationId":"IdentityAuthController_resendVerification","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityResendVerificationDto"}}}},"responses":{"200":{"description":"Verification email sent","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"401":{"description":"Invalid publishable key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid publishable key","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/resend-verification","method":"POST"}}}}}},"summary":"Resend identity verification email","tags":["identity-auth"]}},"/v1/identity/auth/register":{"post":{"description":"Self-service end-user signup into the Environment named by `publishable_key`, gated per-Environment by the self-signup toggle, the allowed-web-origins allowlist (matched against the request `Origin`), CAPTCHA, and a daily signup cap. Creates the identity with a password and emails a verification code rather than returning tokens — the response is always the same generic submitted message, every branch pays the same breach-check + hashing cost, and every branch sends exactly one email (a brand-new address gets the verification code; an already-registered address gets a registration-attempt notice), so neither the response, its timing, nor mail volume reveals whether the address was registered. Rate-limited to 5 per hour; `400` on a breached password, `401` on an invalid publishable key, `403` when signup is disabled / origin is not allowlisted / CAPTCHA fails, `429` when the route throttle or daily cap is exceeded.","operationId":"IdentityAuthController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRegisterDto"}}}},"responses":{"200":{"description":"Registration submitted — verification email sent (if eligible)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"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":"/v1/identity/auth/register","method":"POST"}}}}},"401":{"description":"Invalid publishable key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid publishable key","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/register","method":"POST"}}}}},"403":{"description":"Forbidden — self-service signup is not enabled for this Environment, the request Origin is not allowlisted, or required CAPTCHA verification failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"Forbidden — self-service signup is not enabled for this Environment, the request Origin is not allowlisted, or required CAPTCHA verification failed","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/register","method":"POST"}}}}},"429":{"description":"Too many requests — the per-route signup throttle or the Environment's daily signup cap was exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":429,"code":null,"message":"Too many requests — the per-route signup throttle or the Environment's daily signup cap was exceeded","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/register","method":"POST"}}}}}},"summary":"Self-service identity signup","tags":["identity-auth"]}},"/v1/identity/auth/forgot-password":{"post":{"description":"Starts the link-based password-reset flow: resolves the Account from `publishable_key` and emails a reset link to the address if a matching identity exists. Always returns the same generic response whether or not the account exists (anti-enumeration). Rate-limited to 5 per hour; `401` on an invalid publishable key.","operationId":"IdentityAuthController_forgotPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityForgotPasswordDto"}}}},"responses":{"200":{"description":"Reset email sent (if account exists)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"401":{"description":"Invalid publishable key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Invalid publishable key","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/forgot-password","method":"POST"}}}}}},"summary":"Request identity password reset","tags":["identity-auth"]}},"/v1/identity/auth/forgot-password-code":{"post":{"description":"Starts the OTP-code password-reset flow (the self-signup counterpart to the link-based reset): resolves the Environment from `publishable_key` and emails a short reset code to the address if eligible, returning the same generic response either way. Rate-limited to 5 per hour.","operationId":"IdentityAuthController_forgotPasswordCode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityForgotPasswordDto"}}}},"responses":{"200":{"description":"Reset code sent (if account exists)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}}},"summary":"Request a self-signup password-reset OTP code","tags":["identity-auth"]}},"/v1/identity/auth/reset-password-code":{"post":{"description":"Completes an OTP-code password reset by verifying the emailed `code` for the given email against the Environment named by `publishable_key` and setting the new password. Attempt-limited so the short code can't be brute-forced. Rate-limited to 10 per 10 minutes; `400` on an invalid or expired reset code.","operationId":"IdentityAuthController_resetPasswordCode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityResetPasswordCodeDto"}}}},"responses":{"200":{"description":"Password reset","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired reset code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired reset code","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/reset-password-code","method":"POST"}}}}}},"summary":"Reset identity password with an OTP code","tags":["identity-auth"]}},"/v1/identity/auth/reset-password":{"post":{"description":"Completes a link-based password reset by validating the single-use reset `token` and setting the new password. Public and unauthenticated. Rate-limited to 5 per hour; `400` on an invalid or expired reset token.","operationId":"IdentityAuthController_resetPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityResetPasswordDto"}}}},"responses":{"200":{"description":"Password reset","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired reset token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired reset token","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/reset-password","method":"POST"}}}}}},"summary":"Reset identity password with token","tags":["identity-auth"]}},"/v1/identity/auth/change-password":{"post":{"description":"Changes the authenticated identity's password (guarded by `IdentityJwtGuard`) after re-verifying `current_password`; the prior hash is recorded in password history before the new password is stored. Rate-limited to 5 per hour; `401` when the current password is wrong or the bearer is missing/invalid, `403` when the principal is not an identity.","operationId":"IdentityAuthController_changePassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityChangePasswordDto"}}}},"responses":{"200":{"description":"Password changed","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":"/v1/identity/auth/change-password","method":"POST"}}}}},"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":"/v1/identity/auth/change-password","method":"POST"}}}}}},"summary":"Change identity password (authenticated)","tags":["identity-auth"]}},"/v1/identity/auth/change-email":{"post":{"description":"Requests a self-service email change for the authenticated identity (guarded by `IdentityJwtGuard`): re-authenticates the live session (current password for password identities, plus an MFA step-up via the `x-mfa-step-up-token` header when factors are enrolled), stages the address in `pending_email`, and emails a 6-digit confirmation code to the new address while notifying the current one. Nothing changes on the live email until confirmation. SSO-managed identities are rejected. Rate-limited to 5 per hour; `400` on a malformed/same email, `409` when the new email is already in use.","operationId":"IdentityAuthController_requestEmailChange","parameters":[{"name":"x-mfa-step-up-token","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRequestEmailChangeDto"}}}},"responses":{"200":{"description":"Confirmation code sent to the new email","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid request — the new email is malformed or matches the current email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid request — the new email is malformed or matches the current email","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/change-email","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":"/v1/identity/auth/change-email","method":"POST"}}}}},"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":"/v1/identity/auth/change-email","method":"POST"}}}}},"409":{"description":"The new email is already in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The new email is already in use","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/change-email","method":"POST"}}}}}},"summary":"Request an email change (authenticated) — emails a confirmation code to the new address","tags":["identity-auth"]}},"/v1/identity/auth/change-email/confirm":{"post":{"description":"Confirms a staged email change for the authenticated identity (guarded by `IdentityJwtGuard`) by verifying the 6-digit `code` sent to the pending address, then swaps `email` and marks it verified. Re-checks uniqueness at confirm time and signs the identity out of every other session — revoking all refresh tokens except the current device's. Rate-limited to 10 per 10 minutes; `400` on an invalid/expired code or no pending change, `409` if the address was taken in the interim.","operationId":"IdentityAuthController_confirmEmailChange","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConfirmEmailChangeDto"}}}},"responses":{"200":{"description":"Email changed","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired confirmation code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired confirmation code","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/change-email/confirm","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":"/v1/identity/auth/change-email/confirm","method":"POST"}}}}},"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":"/v1/identity/auth/change-email/confirm","method":"POST"}}}}},"409":{"description":"The new email is already in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The new email is already in use","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/change-email/confirm","method":"POST"}}}}}},"summary":"Confirm an email change with the code sent to the new address","tags":["identity-auth"]}},"/v1/identity/auth/me":{"get":{"description":"Returns the authenticated identity's own profile (id, email, name, and related fields) for the principal resolved from the bearer token (guarded by `IdentityJwtGuard`). The end-user equivalent of a `/me` endpoint. `401`/`403` when the bearer is missing/invalid or is not an identity principal.","operationId":"IdentityAuthController_getProfile","parameters":[],"responses":{"200":{"description":"Profile returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityProfileDto"}}}}}},"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":"/v1/identity/auth/me","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":"/v1/identity/auth/me","method":"GET"}}}}}},"summary":"Get current identity profile","tags":["identity-auth"]}},"/v1/identity/auth/sessions":{"get":{"description":"Lists the authenticated identity's own active sessions — one entry per live refresh token, with device/IP and timestamp metadata for a session-management UI (guarded by `IdentityJwtGuard`). Returned as an unpaginated `{ items }` collection. `401`/`403` when the bearer is missing/invalid or is not an identity principal.","operationId":"IdentityAuthController_listSessions","parameters":[],"responses":{"200":{"description":"Sessions returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IdentitySessionDto"}}}}}}},"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":"/v1/identity/auth/sessions","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":"/v1/identity/auth/sessions","method":"GET"}}}}}},"summary":"List identity active sessions","tags":["identity-auth"]}},"/v1/identity/auth/sessions/{id}":{"delete":{"description":"Revokes one of the authenticated identity's own sessions by its `id` path param, invalidating that refresh token (guarded by `IdentityJwtGuard`). Returns `204 No Content` on success; `401`/`403` when the bearer is missing/invalid or is not an identity principal.","operationId":"IdentityAuthController_revokeSession","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Session revoked"},"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":"/v1/identity/auth/sessions/{id}","method":"DELETE"}}}}},"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":"/v1/identity/auth/sessions/{id}","method":"DELETE"}}}}}},"summary":"Revoke a specific identity session","tags":["identity-auth"]}},"/v1/identity/auth/sessions/revoke-all":{"post":{"description":"\"Sign out everywhere\" — bulk-revokes every active refresh token for the authenticated identity, including the current session, so the next `/refresh` on any device returns `401` until the identity signs in again (guarded by `IdentityJwtGuard`). In-flight access tokens keep working until their short TTL expires. Throttled to 5 per minute per identity so a stolen JWT can't be used to mass-revoke as a denial-of-service. Returns `204 No Content`.","operationId":"IdentityAuthController_revokeAllSessions","parameters":[],"responses":{"204":{"description":"Revokes every active refresh token for the identity, including the current session. Subsequent calls to /v1/identity/auth/refresh on any device return 401 until the identity signs in again."},"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":"/v1/identity/auth/sessions/revoke-all","method":"POST"}}}}},"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":"/v1/identity/auth/sessions/revoke-all","method":"POST"}}}}}},"summary":"Sign out everywhere for the authenticated identity","tags":["identity-auth"]}},"/v1/identity/auth/invite-info":{"post":{"description":"Public lookup of a pending admin invite by its raw `token`, so the accept-invite page can prefill the form. `POST` (rate-limited to 10 per minute) returning the invitee's `email`, `first_name`, `last_name`, and an `sso_available` flag that is true when the Account has an active admin SSO connection covering the invitee's domain. Returns `400` when the token is unknown, expired, or the invite is no longer pending.","operationId":"IdentityAuthController_getInviteInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteInfoRequestDto"}}}},"responses":{"200":{"description":"Invite details returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/InviteInfoResponseDto"}}}}}},"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":"/v1/identity/auth/invite-info","method":"POST"}}}}},"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":"/v1/identity/auth/invite-info","method":"POST"}}}}}},"summary":"Look up invite details by token","tags":["identity-auth"]}},"/v1/identity/auth/accept-invite":{"post":{"description":"Public endpoint that redeems an admin invite token and provisions the invitee as an active, email-verified `User` (or updates an existing unverified one), then adds the `AccountMember` row joining them to the inviting Account. `POST` accepting `token`, `first_name`, `last_name`, and an optional `password`; rate-limited to 5 per hour. A password is required and breach-checked unless the Account has active admin SSO for the invitee's domain, in which case password-less acceptance is allowed. Returns `400` for an invalid, expired, or already-consumed token, or a missing password without SSO.","operationId":"IdentityAuthController_acceptInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityAcceptInviteDto"}}}},"responses":{"200":{"description":"Invite accepted, user created or updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MessageResponseDto"}}}}}},"400":{"description":"Invalid or expired invitation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Invalid or expired invitation","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/accept-invite","method":"POST"}}}}},"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":"/v1/identity/auth/accept-invite","method":"POST"}}}}},"409":{"description":"A pending invite already exists for this email at this scope. Revoke the existing invite or wait for it to expire before issuing a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"A pending invite already exists for this email at this scope. Revoke the existing invite or wait for it to expire before issuing a new one.","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/accept-invite","method":"POST"}}}}}},"summary":"Accept an invite","tags":["identity-auth"]}},"/v1/identity/auth/mfa/factors":{"get":{"description":"Lists the authenticated identity's enrolled MFA factors as an `{ items }` array, including TOTP authenticators and WebAuthn credentials with their labels and creation metadata. Read-only; reflects the caller's own factors only, derived from the bearer `JWT` principal.","operationId":"IdentityMfaController_listFactors","parameters":[],"responses":{"200":{"description":"Returns one row per enrolled TOTP or WebAuthn factor. Secrets and credential public keys are never echoed back.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MfaFactorResponseDto"}}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List enrolled MFA factors for the caller","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/totp/enroll/start":{"post":{"description":"Begins TOTP enrollment by generating a new authenticator secret and returning the provisioning `otpauth://` URI plus a short-lived `enrollment_token` that must be echoed back to the verify step. Does not yet activate the factor — the secret is only persisted once a valid code confirms it. Rate-limited to 5 requests per minute.","operationId":"IdentityMfaController_startTotpEnrollment","parameters":[],"responses":{"200":{"description":"Generates a provisional TOTP secret, the `otpauth://` URI for the authenticator app, a base64 QR data URL, and a short-lived sealed `enrollment_token` that must be returned to the verify endpoint within 5 minutes. The factor row is NOT created until verify succeeds.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaTotpEnrollStartResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Begin TOTP factor enrollment","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/totp/enroll/verify":{"post":{"description":"Completes TOTP enrollment by validating the 6-digit `code` against the secret bound to the supplied `enrollment_token`, then persisting the factor under the optional `label`. Returns the created factor and, when this is the identity's first factor, a one-time batch of recovery codes shown only here. Responds `400` when the `enrollment_token` is missing, expired, or the code does not match.","operationId":"IdentityMfaController_verifyTotpEnrollment","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaTotpEnrollVerifyDto"}}}},"responses":{"200":{"description":"Verifies a 6-digit code against the secret carried by the enrollment token and persists the factor. Returns the 10 single-use recovery codes — this is the only time they are shown in plaintext, and only on the first factor enrollment (subsequent enrollments return `recovery_codes: null` so the existing batch stays valid).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaTotpEnrollVerifyResponseDto"}}}}}},"400":{"description":"Enrollment token is missing, malformed, or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Enrollment token is missing, malformed, or expired","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/totp/enroll/verify","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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Complete TOTP factor enrollment","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/webauthn/enroll/options":{"post":{"description":"Starts WebAuthn (passkey/security-key) enrollment by returning the `navigator.credentials.create()` `PublicKeyCredentialCreationOptions` (challenge, relying-party, excluded credentials) alongside a short-lived `enrollment_token`. The attestation produced by the authenticator is submitted to the verify step. Rate-limited to 5 requests per minute.","operationId":"IdentityMfaController_startWebAuthnEnrollment","parameters":[],"responses":{"200":{"description":"Generates `PublicKeyCredentialCreationOptions` for `navigator.credentials.create()` and a sealed 5-min enrollment_token carrying the WebAuthn challenge nonce. Browser passes the options to the platform authenticator; the resulting attestation goes back to /verify.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaWebAuthnEnrollOptionsResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Begin WebAuthn factor enrollment","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/webauthn/enroll/verify":{"post":{"description":"Completes WebAuthn enrollment by verifying the authenticator `response` (attestation) against the challenge bound to the `enrollment_token`, then persisting the credential as a factor under the optional `label`. Returns the created factor and, when this is the identity's first factor, a one-time batch of recovery codes. Responds `400` when the attestation is invalid or the `enrollment_token` is missing or expired.","operationId":"IdentityMfaController_verifyWebAuthnEnrollment","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaWebAuthnEnrollVerifyDto"}}}},"responses":{"200":{"description":"Verifies the attestation against the stored challenge nonce, persists the WebAuthn factor (credential id, public key, counter, transports, aaguid), and returns the 10 single-use recovery codes when this is the identity's first factor.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaWebAuthnEnrollVerifyResponseDto"}}}}}},"400":{"description":"WebAuthn attestation failed to verify (challenge mismatch, RP-ID mismatch, signature invalid, or credential ineligible)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"WebAuthn attestation failed to verify (challenge mismatch, RP-ID mismatch, signature invalid, or credential ineligible)","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/webauthn/enroll/verify","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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Complete WebAuthn factor enrollment","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/step-up":{"post":{"description":"Performs an in-session MFA step-up for an already-authenticated identity by verifying a `totp` or `recovery_code` `factor`, returning a short-lived step-up token used to authorize sensitive operations such as removing a factor or regenerating recovery codes. Distinct from the login-time challenge flow — the caller is already holding a valid session `JWT`. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaController_stepUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaStepUpDto"}}}},"responses":{"200":{"description":"Verifies a TOTP code or single-use recovery code from the caller's enrolled factors and returns a short-lived (5 min) sealed `step_up_token` that DELETE /mfa/factors/:id and POST /mfa/recovery-codes/regenerate consume via the X-Mfa-Step-Up-Token header. On failure, the 401 response body's `error.code` is `mfa.step_up_invalid` (factor verification failed) or `auth.invalid_token` (caller's bearer token is invalid).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaStepUpResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Prove a fresh factor to authorise a sensitive MFA mutation","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/step-up/webauthn/options":{"post":{"description":"Starts a WebAuthn step-up for the authenticated identity by returning the `navigator.credentials.get()` assertion options (challenge and allowed credentials) plus a transit token tying the challenge to the caller. The signed assertion is submitted to the WebAuthn step-up verify endpoint. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaController_startWebAuthnStepUp","parameters":[],"responses":{"200":{"description":"Generates `PublicKeyCredentialRequestOptions` for the caller's enrolled WebAuthn factors plus a sealed 5-min `transit_token` carrying the cryptographic nonce. Browser passes the options to `navigator.credentials.get()`; the assertion goes back to /verify along with the transit_token.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaStepUpWebAuthnOptionsResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Begin a WebAuthn-backed step-up ceremony","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/step-up/webauthn/verify":{"post":{"description":"Completes a WebAuthn step-up by verifying the authenticator `response` against the challenge bound to the `transit_token`, issuing the same short-lived step-up token as the TOTP/recovery-code step-up path. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaController_verifyWebAuthnStepUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaStepUpWebAuthnVerifyDto"}}}},"responses":{"200":{"description":"Verifies the assertion against the stored nonce + the factor row's credential and returns the same `step_up_token` shape that POST /mfa/step-up produces. The factor row's signature counter is bumped to defeat replay.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaStepUpResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Complete a WebAuthn step-up ceremony","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/factors/{id}":{"delete":{"description":"Removes one enrolled MFA factor by `id` and returns `204 No Content`. Requires a valid step-up token in the `x-mfa-step-up-token` header — the request is rejected if it is absent or does not match the caller. Responds `404` when the factor does not exist or belongs to another identity.","operationId":"IdentityMfaController_removeFactor","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-mfa-step-up-token","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"204":{"description":"Deletes the factor row. Requires a fresh `X-Mfa-Step-Up-Token` proving the caller still controls a factor — this prevents a session-hijack from silently disenrolling the user's second factor. On 401, the body's `error.code` is `mfa.step_up_required` (header missing or invalid) or `auth.invalid_token` (caller's bearer token is invalid)."},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}},"404":{"description":"Factor not found or does not belong to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Factor not found or does not belong to the caller","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/factors/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove an enrolled MFA factor","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/recovery-codes/regenerate":{"post":{"description":"Generates a fresh batch of single-use recovery codes, invalidating any previously issued batch, and returns the plaintext codes together with the new `recovery_codes_generation` counter. The codes are shown only in this response and cannot be retrieved again. Requires a valid step-up token in the `x-mfa-step-up-token` header; rate-limited to 5 requests per minute.","operationId":"IdentityMfaController_regenerateRecoveryCodes","parameters":[{"name":"x-mfa-step-up-token","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Mints a fresh batch of 10 codes (incrementing the generation), invalidates every unredeemed code in the prior generation, and returns the new plaintext codes ONCE. Requires step-up. On 401, the body's `error.code` is `mfa.step_up_required` (header missing or invalid) or `auth.invalid_token` (caller's bearer token is invalid).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaRecoveryCodesResponseDto"}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Regenerate the identity's single-use recovery codes","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/trusted-devices":{"get":{"description":"Lists the authenticated identity's remembered (trusted) devices as an `{ items }` array — the browsers that previously opted to skip MFA via the remember-device flow. Each entry carries identifying metadata and expiry. Scoped to the caller's own devices.","operationId":"IdentityMfaController_listTrustedDevices","parameters":[],"responses":{"200":{"description":"One row per active trusted device, ordered by `last_seen_at` descending. Cookie hashes are not echoed back.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MfaTrustedDeviceResponseDto"}}}}}}},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the identity's active 'remember this device' records","tags":["identity-mfa"]},"delete":{"description":"Revokes every trusted device for the authenticated identity and clears the trusted-device cookie on the current response, so even the calling browser must re-challenge next time. Returns `204 No Content`.","operationId":"IdentityMfaController_revokeAllTrustedDevices","parameters":[],"responses":{"204":{"description":"Deletes every row for the identity, regardless of env. Use this for 'sign out everywhere'-style flows."},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke every trusted device for the caller","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/trusted-devices/{id}":{"delete":{"description":"Revokes a single trusted device by `id`, forcing that device to satisfy a full MFA challenge on its next login, and returns `204 No Content`. Responds `404` when the device does not exist or belongs to another identity.","operationId":"IdentityMfaController_revokeTrustedDevice","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Deletes the row. The matching cookie continues to ride on subsequent requests but is rejected at verify; the client should also clear the local cookie if the user is on that device."},"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":"/v1/identity/auth/mfa/factors","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":"/v1/identity/auth/mfa/factors","method":"GET"}}}}},"404":{"description":"Trusted device not found or does not belong to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Trusted device not found or does not belong to the caller","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/trusted-devices/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke a single trusted device","tags":["identity-mfa"]}},"/v1/identity/auth/mfa/challenge/totp":{"post":{"description":"Completes a login-time MFA challenge by verifying a TOTP `code` against the pending `challenge_token` issued by the prior `/login` step. On success mints a fully-bound identity session and returns the standard login response; when `remember_device` is true and the environment permits it, a trusted-device cookie is also set. Public (token-authenticated via the body); responds `401` when the challenge token or code is invalid. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaChallengeController_challengeTotp","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeTotpDto"}}}},"responses":{"200":{"description":"Verifies the 6-digit code from the authenticator app against an enrolled TOTP factor. Returns the full session (access + refresh + identity + application) on success.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/totp","method":"POST"}}}}}},"summary":"Submit a TOTP code to satisfy an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/v1/identity/auth/mfa/challenge/recovery-code":{"post":{"description":"Completes a login-time MFA challenge by consuming a single-use recovery `code` against the pending `challenge_token`, used when the primary factor is unavailable. The code is burned on success; the call then mints the identity session and returns the standard login response, optionally setting a trusted-device cookie when `remember_device` is true. Responds `401` when the challenge token or recovery code is invalid. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaChallengeController_challengeRecoveryCode","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeRecoveryCodeDto"}}}},"responses":{"200":{"description":"Consumes one of the 10 recovery codes issued at enrollment. Returns the full session on success. The redeemed code is marked single-use and cannot satisfy a future challenge.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/recovery-code","method":"POST"}}}}}},"summary":"Redeem a single-use recovery code to satisfy an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/v1/identity/auth/mfa/challenge/webauthn/options":{"post":{"description":"Returns the `navigator.credentials.get()` assertion options (challenge and allowed credentials) for a login-time WebAuthn challenge, derived from the identity bound to the supplied `challenge_token`. The signed assertion is submitted to the WebAuthn challenge verify endpoint. Public (token-authenticated via the body); responds `401` when the challenge token is invalid. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaChallengeController_webauthnOptions","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeWebAuthnOptionsDto"}}}},"responses":{"200":{"description":"Returns `PublicKeyCredentialRequestOptions` for `navigator.credentials.get()`. The challenge row's WebAuthn nonce is persisted server-side and consumed by /verify.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MfaChallengeWebAuthnOptionsResponseDto"}}}}}},"400":{"description":"No passkey can answer this challenge — the identity has none enrolled, `env.settings.mfa_factor_allowlist` excludes `webauthn`, or every enrolled passkey is bound to a different relying party than the surface being used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"No passkey can answer this challenge — the identity has none enrolled, `env.settings.mfa_factor_allowlist` excludes `webauthn`, or every enrolled passkey is bound to a different relying party than the surface being used","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/webauthn/options","method":"POST"}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/webauthn/options","method":"POST"}}}}}},"summary":"Generate WebAuthn authentication options for an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/v1/identity/auth/mfa/challenge/webauthn/verify":{"post":{"description":"Completes a login-time WebAuthn challenge by verifying the authenticator `response` against the challenge bound to the `challenge_token`, then minting the identity session and returning the standard login response. Sets a trusted-device cookie when `remember_device` is true and the environment allows it. Responds `401` when the challenge token or assertion is invalid. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaChallengeController_challengeWebAuthn","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeWebAuthnVerifyDto"}}}},"responses":{"200":{"description":"Verifies the WebAuthn assertion against the stored challenge + the factor row's credential. Returns the full session on success with `amr: ['pwd', 'mfa', 'hwk']`. The factor row's signature counter is bumped to defeat replay attacks.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"400":{"description":"No passkey can answer this challenge — the identity has none enrolled, `env.settings.mfa_factor_allowlist` excludes `webauthn`, or every enrolled passkey is bound to a different relying party than the surface being used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"No passkey can answer this challenge — the identity has none enrolled, `env.settings.mfa_factor_allowlist` excludes `webauthn`, or every enrolled passkey is bound to a different relying party than the surface being used","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/webauthn/verify","method":"POST"}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/webauthn/verify","method":"POST"}}}}}},"summary":"Submit a WebAuthn assertion to satisfy an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/v1/identity/auth/mfa/challenge/email-otp/send":{"post":{"description":"Sends a one-time email passcode for the login-time MFA challenge identified by `challenge_token`, returning `204 No Content` regardless of delivery details. Responds `400` when email OTP is not available for the challenge or when the per-challenge send limit is exceeded, and `401` when the challenge token is invalid. Heavily rate-limited to 3 requests per minute.","operationId":"IdentityMfaChallengeController_sendEmailOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeEmailOtpSendDto"}}}},"responses":{"204":{"description":"Generates an 8-digit code, persists its hash on the challenge row, and emails the plaintext to the identity's verified address. Rate-limited to one send per 60s per challenge. Returns 204."},"400":{"description":"Email OTP cannot be issued — the identity has no verified email, or env.settings.mfa_factor_allowlist excludes email_otp","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Email OTP cannot be issued — the identity has no verified email, or env.settings.mfa_factor_allowlist excludes email_otp","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/email-otp/send","method":"POST"}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/email-otp/send","method":"POST"}}}}}},"summary":"Trigger an email OTP for an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/v1/identity/auth/mfa/challenge/email-otp/verify":{"post":{"description":"Completes the login-time MFA challenge by verifying the emailed `code` against the pending `challenge_token`, then minting the identity session and returning the standard login response. Sets a trusted-device cookie when `remember_device` is true and the environment permits it. Responds `401` when the challenge token or code is invalid. Rate-limited to 10 requests per minute.","operationId":"IdentityMfaChallengeController_verifyEmailOtp","parameters":[{"name":"X-API-Key","in":"header","description":"Optional secret API key for trusted backend (BFF) callers. When a valid secret key scoped to this environment is presented, the session's `refresh_token` is returned in the JSON response body (so the backend can hold the session server-side) and NO httpOnly cookie is set. Omit it for browser callers — the refresh token then rides exclusively in the `ca_identity_refresh_token` cookie. A browser holds only the non-secret publishable key, so it can never unlock body delivery.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeEmailOtpVerifyDto"}}}},"responses":{"200":{"description":"Verifies the 8-digit code against the stored hash + TTL. Returns the full session on success with `amr: ['pwd', 'mfa', 'otp']`.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityLoginResponseDto"}}}}}},"401":{"description":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":401,"code":null,"message":"Challenge token is missing, expired, already consumed, or locked after too many failed attempts","timestamp":"2026-04-20T12:00:00.000Z","path":"/v1/identity/auth/mfa/challenge/email-otp/verify","method":"POST"}}}}}},"summary":"Submit an email OTP to satisfy an in-flight MFA challenge","tags":["identity-mfa-challenge"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients":{"post":{"description":"Registers a new confidential OAuth client in the current Environment with the given `name`, `redirect_uris`, and optional `scopes` (defaulting to `openid profile email`). Generates the `client_id` and a `client_secret` that is bcrypt-hashed at rest; the raw secret is returned in this response only and is never retrievable again. Responds `201 Created`.","operationId":"OAuthClientsController_createClient","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOAuthClientDto"}}}},"responses":{"201":{"description":"Client created — client_secret is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientCreatedResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Register a new OAuth client","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.manage","description":"Create, edit, rotate, and delete OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"get":{"description":"Returns a paginated list of the OAuth clients registered in the current Environment, newest first by default. Supports `q` to filter by client name and the standard sort/pagination query options. Client secrets are never included.","operationId":"OAuthClientsController_listClients","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/OAuthClientResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List OAuth clients for Application","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.view","description":"View OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/summary":{"get":{"description":"Returns Environment-wide active and inactive OAuth client counts (not just the current page), optionally narrowed by the same `q` name filter the list endpoint uses. Drives the client stat card.","operationId":"OAuthClientsController_summary","parameters":[{"name":"q","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth client active/inactive summary returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ActiveSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Active/inactive OAuth client counts for the environment","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.view","description":"View OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}":{"get":{"description":"Returns a single OAuth client by `id`, scoped to the current Environment — a client that exists in a sibling Environment reads as `404`. The `client_secret` is never included.","operationId":"OAuthClientsController_getClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth client returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}},"404":{"description":"OAuth client not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"OAuth client not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get OAuth client details","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.view","description":"View OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates an OAuth client's mutable fields (`name`, `redirect_uris`, `scopes`, `is_active`, `invite_redirect_url`), scoped to the current Environment. Returns the updated client, or `404` when the client is not found in this Environment. The secret is not rotated here.","operationId":"OAuthClientsController_updateClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOAuthClientDto"}}}},"responses":{"200":{"description":"OAuth client updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}},"404":{"description":"OAuth client not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"OAuth client not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update OAuth client","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.manage","description":"Create, edit, rotate, and delete OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes an OAuth client from the current Environment by `id`. Responds `204 No Content` on success, or `404` when the client is not found in this Environment.","operationId":"OAuthClientsController_deleteClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"OAuth client 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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}},"404":{"description":"OAuth client not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"OAuth client not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete OAuth client","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.manage","description":"Create, edit, rotate, and delete OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}/rotate-secret":{"post":{"description":"Rotates the OAuth client's `client_secret`, generating and hashing a new secret and invalidating the old one. The new raw secret is returned in this response only and cannot be retrieved again. Returns `404` when the client is not found in the current Environment.","operationId":"OAuthClientsController_rotateClientSecret","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"OAuth client secret rotated — the new secret is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientSecretRotatedResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients","method":"GET"}}}}},"404":{"description":"OAuth client not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"OAuth client not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/oauth-clients/{id}/rotate-secret","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Rotate an OAuth client's secret","tags":["oauth-clients"],"x-canopy-required-authorization":{"capability":{"key":"oauth_clients.manage","description":"Create, edit, rotate, and delete OAuth clients","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}":{"get":{"description":"Returns the current Account's profile — `id`, `name`, `slug`, `settings`, `plan`, and the optimistic-lock `version`. Account scope comes from `:accountSlug` resolved by `AccountAuthGuard`; the caller must be a member. Pass the returned `version` back as the `If-Match` header on a subsequent update. Returns `404` if the Account does not exist or has been soft-deleted.","operationId":"PortalAccountsController_getAccount","parameters":[],"responses":{"200":{"description":"Account returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}","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":"/portal/v1/accounts/{accountSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the current Account","tags":["portal-accounts"]},"patch":{"description":"Updates the Account's display `name` and/or `settings` JSON; both body fields are optional and only what's provided is changed. `settings` replaces the whole object, so merge client-side to change a single key. `slug` is intentionally not editable here. Supply the `version` you read via the `If-Match` header for optimistic locking — a mismatch returns `409` with the current row. Writes an `account.updated` audit row.","operationId":"PortalAccountsController_updateAccount","parameters":[{"name":"accountSlug","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 token — pass the `version` you read on GET. Mismatch returns 409.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAccountDto"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}","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":"/portal/v1/accounts/{accountSlug}","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":"/portal/v1/accounts/{accountSlug}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update the Account name or settings","tags":["portal-accounts"],"x-canopy-required-authorization":{"capability":{"key":"account.manage","description":"Edit account profile and general settings","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Owner-only soft-delete of the Account. Marks the row `deleted_at` with a `purge_after` deadline (default 30-day grace), revokes every member's session so access stops immediately, and emits the `account.deleted` event that cancels billing on the hosted instance. The row is recoverable during the grace window before a daily cron hard-purges it. Returns `204` on success and `403` (`account.delete_owner_only`) when the caller is a member but not the owner. Writes an `account.deleted` audit row.","operationId":"PortalAccountsController_deleteAccount","parameters":[{"name":"accountSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Account soft-deleted: access stops immediately, paid billing is cancelled, and the account is scheduled for permanent purge after a grace window. Owner-only."},"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":"/portal/v1/accounts/{accountSlug}","method":"GET"}}}}},"403":{"description":"Only the account owner can delete the account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"Only the account owner can delete the account","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete the Account (owner only)","tags":["portal-accounts"],"x-canopy-required-authorization":{"capability":{"key":"account.delete","description":"Delete the account","category":"Danger zone"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/admin-roles":{"get":{"operationId":"PortalAdminRolesController_listRoles","parameters":[],"responses":{"200":{"description":"Administrator roles returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminRoleResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List operational-administrator roles for the Account","tags":["portal-admin-roles"]},"post":{"operationId":"PortalAdminRolesController_createRole","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdminRoleDto"}}}},"responses":{"201":{"description":"Administrator role created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AdminRoleResponseDto"}}}}}},"400":{"description":"One or more capabilities are invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"One or more capabilities are invalid","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles","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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"409":{"description":"An administrator role with that name exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"An administrator role with that name exists","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create an operational-administrator role","tags":["portal-admin-roles"]}},"/portal/v1/accounts/{accountSlug}/admin-roles/capabilities":{"get":{"operationId":"PortalAdminRolesController_listCapabilities","parameters":[{"name":"include_owner_only","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Operational capability catalog returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminCapabilityDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the operational capabilities a role may grant","tags":["portal-admin-roles"]}},"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}":{"get":{"operationId":"PortalAdminRolesController_getRole","parameters":[{"name":"adminRoleId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Administrator role returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AdminRoleResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an operational-administrator role","tags":["portal-admin-roles"]},"patch":{"operationId":"PortalAdminRolesController_updateRole","parameters":[{"name":"adminRoleId","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/UpdateAdminRoleDto"}}}},"responses":{"200":{"description":"Administrator role updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AdminRoleResponseDto"}}}}}},"400":{"description":"One or more capabilities are invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"One or more capabilities are invalid","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"403":{"description":"System administrator roles cannot be modified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"System administrator roles cannot be modified","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"PATCH"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","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":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an operational-administrator role","tags":["portal-admin-roles"]},"delete":{"operationId":"PortalAdminRolesController_deleteRole","parameters":[{"name":"adminRoleId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Administrator 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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"403":{"description":"System administrator roles cannot be modified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"System administrator roles cannot be modified","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"DELETE"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"DELETE"}}}}},"409":{"description":"Role is still granted to people; revoke those assignments first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Role is still granted to people; revoke those assignments first","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an administrator role","tags":["portal-admin-roles"]}},"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}/grantable-roles":{"get":{"operationId":"PortalAdminRolesController_listGrantableRoles","parameters":[{"name":"adminRoleId","required":true,"in":"path","schema":{"type":"string"}},{"name":"environmentId","required":true,"in":"query","description":"Environment whose grantable roles to read","schema":{"type":"string"}}],"responses":{"200":{"description":"Grantable identity role IDs 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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}/grantable-roles","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the identity roles an administrator role may grant in an Environment","tags":["portal-admin-roles"]},"put":{"operationId":"PortalAdminRolesController_setGrantableRoles","parameters":[{"name":"adminRoleId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetGrantableRolesDto"}}}},"responses":{"200":{"description":"Grantable identity roles updated","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"One or more roles do not exist in the Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"One or more roles do not exist in the Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}/grantable-roles","method":"PUT"}}}}},"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":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-roles","method":"GET"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-roles/{adminRoleId}/grantable-roles","method":"PUT"}}}}}},"security":[{"JWT":[]}],"summary":"Set the identity roles an administrator role may grant in an Environment","tags":["portal-admin-roles"]}},"/portal/v1/accounts/{accountSlug}/workspace-environments":{"get":{"operationId":"PortalWorkspaceEnvironmentsController_listWorkspaceEnvironments","parameters":[],"responses":{"200":{"description":"Operable Environments returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceEnvironmentResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/workspace-environments","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":"/portal/v1/accounts/{accountSlug}/workspace-environments","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the Environments the caller may operate in","tags":["portal-workspace-environments"]}},"/portal/v1/accounts/{accountSlug}/admin-assignments":{"get":{"operationId":"PortalAdminAssignmentsController_listAssignments","parameters":[],"responses":{"200":{"description":"Administrator assignments returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminAssignmentResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/admin-assignments","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":"/portal/v1/accounts/{accountSlug}/admin-assignments","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List operational administrators in an Environment","tags":["portal-admin-assignments"]},"post":{"operationId":"PortalAdminAssignmentsController_createAssignment","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdminAssignmentDto"}}}},"responses":{"201":{"description":"Administrator assignment created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AdminAssignmentResponseDto"}}}}}},"400":{"description":"Node and Environment scope are inconsistent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Node and Environment scope are inconsistent","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-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":"/portal/v1/accounts/{accountSlug}/admin-assignments","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":"/portal/v1/accounts/{accountSlug}/admin-assignments","method":"GET"}}}}},"404":{"description":"Administrator role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator role not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-assignments","method":"POST"}}}}},"409":{"description":"An identical live grant already covers this period","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"An identical live grant already covers this period","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-assignments","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Grant an operational administrator","tags":["portal-admin-assignments"]}},"/portal/v1/accounts/{accountSlug}/admin-assignments/{adminAssignmentId}":{"delete":{"operationId":"PortalAdminAssignmentsController_revokeAssignment","parameters":[{"name":"adminAssignmentId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Administrator assignment revoked"},"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":"/portal/v1/accounts/{accountSlug}/admin-assignments","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":"/portal/v1/accounts/{accountSlug}/admin-assignments","method":"GET"}}}}},"404":{"description":"Administrator assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Administrator assignment not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/admin-assignments/{adminAssignmentId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke an operational administrator","tags":["portal-admin-assignments"]}},"/portal/v1/accounts/{accountSlug}/applications":{"get":{"description":"Returns every Application in the Account as an unpaginated `{ items }` list. Account-level surface (no `:appSlug`), authorized by account membership.","operationId":"PortalAccountApplicationsController_listApplications","parameters":[],"responses":{"200":{"description":"Applications returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications","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":"/portal/v1/accounts/{accountSlug}/applications","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List Applications in the Account","tags":["portal-account-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates a new Application in the Account from a `name` and optional `slug` (derived from the name when omitted). In one transaction it inserts the Application and seeds its default Environments, each with its own root hierarchy node. A slug already in use within the Account returns `409` (`application.slug_conflict`). Writes an `application.created` audit row.","operationId":"PortalAccountApplicationsController_createApplication","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApplicationDto"}}}},"responses":{"201":{"description":"Application created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApplicationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications","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":"/portal/v1/accounts/{accountSlug}/applications","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":"/portal/v1/accounts/{accountSlug}/applications","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create a new Application in the Account","tags":["portal-account-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections":{"get":{"description":"Lists the SSO connections owned by the current Account, scoped to `ctx.accountId`. Accepts an optional `scope` query parameter (`admin` or `end_user`) to filter by connection scope; any other value is ignored and all connections are returned. Each entry covers SAML or OIDC config with secrets redacted, and the response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_list","parameters":[{"name":"scope","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"All SSO connections for the Account. Optional `?scope=admin|end_user` filter.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List SSO connections","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates a new SSO connection in the Account in `configuring` status, accepting SAML or OIDC config via the request body; any supplied `oidc_client_secret` is encrypted before persistence and never returned. `end_user`-scope connections consume a plan connection slot and return `409` when the limit is reached, while `admin`-scope connections are unmetered. Emits an `sso.connection.created` audit row and returns the created connection as `201`.","operationId":"PortalSsoConnectionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSsoConnectionDto"}}}},"responses":{"201":{"description":"Created in `configuring` status. Protocol fields can be supplied here or filled in via PATCH; `activate` validates completeness.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"409":{"description":"The Account has reached its plan limit for end-user SSO connections.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The Account has reached its plan limit for end-user SSO connections.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}":{"get":{"description":"Returns a single SSO connection by `id`, scoped to the current Account. Secret material (the OIDC client secret) is never included in the response. Responds `404` when no connection with that id exists in the Account.","operationId":"PortalSsoConnectionsController_detail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Single connection with config + health fields.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates a connection's SAML/OIDC config; only fields present in the body are changed, and passing `oidc_client_secret` re-encrypts it while passing `null` clears it and omitting it leaves the stored secret untouched. If the connection is currently `active`, required-field validation is re-run so an edit cannot leave a live connection unactivatable. Emits an `sso.connection.updated` audit row recording the changed field names; responds `404` when the connection is not found in the Account.","operationId":"PortalSsoConnectionsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSsoConnectionDto"}}}},"responses":{"200":{"description":"Patch config / routing / attribute mapping. `scope` and `type` are immutable. A new `oidc_client_secret` is re-encrypted; passing it null clears it.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Permanently deletes an SSO connection and its configuration from the Account. Emits an `sso.connection.deleted` audit row and returns `204` with no body; responds `404` when the connection is not found.","operationId":"PortalSsoConnectionsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Hard-delete the connection."},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/activate":{"post":{"description":"Promotes a connection from `configuring` to `active` after validating that the type's required fields are present (SAML: entity id, SSO URL, signing cert; OIDC: discovery URL, client id, client secret). Returns `400` when configuration is incomplete and `404` when the connection is not found. Emits an `sso.connection.activated` audit row and returns the updated connection.","operationId":"PortalSsoConnectionsController_activate","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Promote `configuring → active` after validating the type's required fields.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}},"400":{"description":"Connection is missing fields required for activation (SAML: entity ID, SSO URL, signing cert; OIDC: discovery URL, client ID, client secret).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Connection is missing fields required for activation (SAML: entity ID, SSO URL, signing cert; OIDC: discovery URL, client ID, client secret).","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/activate","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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/activate","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Activate an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/disable":{"post":{"description":"Sets an active connection's status to `disabled`, stopping all login flows through it while preserving the row and its configuration for later re-activation. Emits an `sso.connection.disabled_by_user` audit row and returns the updated connection. Responds `404` when the connection is not found in the Account.","operationId":"PortalSsoConnectionsController_disable","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Set status to `disabled`. The row is preserved; auth flows stop until re-activated.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoConnectionResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/disable","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Disable an SSO connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients":{"get":{"description":"Lists the OAuth-client links for an `end_user` SSO connection, ordered oldest-first, each carrying the per-client hierarchy-placement config (`is_default`, `default_hierarchy_node_id`, `claim_node_mapping`, `default_role_id`). Returns `404` when the connection is not found or is not `end_user`-scope. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_listOAuthClientLinks","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The OAuth clients this end-user connection serves, each with its per-client placement config.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/OAuthClientLinkResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List OAuth-client links for an end-user connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients/{oauthClientId}":{"post":{"description":"Links an `end_user` SSO connection to one of the Account's OAuth clients, storing the per-client placement config from the body. Every referenced placement target (each node and the `default_role_id`) must belong to the OAuth client's Environment, otherwise `400`; setting `is_default` clears any other default for the same client in the same transaction. Returns `400` if the connection is not `end_user`-scope, `404` if the connection or client is not in the Account, emits an `sso.connection.oauth_client.linked` audit row, and returns `201`.","operationId":"PortalSsoConnectionsController_linkOAuthClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"oauthClientId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkOAuthClientDto"}}}},"responses":{"201":{"description":"Bind an end-user connection to an OAuth client with per-client placement config (default node, claim rules, default role). `is_default` sends that client's users straight to the IdP.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientLinkResponseDto"}}}}}},"400":{"description":"Only end-user SSO connections can be linked to OAuth clients.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Only end-user SSO connections can be linked to OAuth clients.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients/{oauthClientId}","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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients/{oauthClientId}","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Link an OAuth client to an end-user connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates the placement config of an existing OAuth-client link; only fields present in the body are changed, and placement nodes/role are re-validated against the client's Environment (`400` on mismatch). Switching `is_default` to true clears any other default for the same client in the same transaction. Emits an `sso.connection.oauth_client.updated` audit row; responds `404` when the link does not exist.","operationId":"PortalSsoConnectionsController_updateOAuthClientLink","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"oauthClientId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOAuthClientLinkDto"}}}},"responses":{"200":{"description":"Patch the link's `is_default` flag or placement config. Setting `is_default` unsets any other default for the same client.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OAuthClientLinkResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"This OAuth client is not linked to the connection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"This OAuth client is not linked to the connection.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients/{oauthClientId}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an OAuth-client link","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Removes the link between an `end_user` SSO connection and an OAuth client, leaving both records intact. Emits an `sso.connection.oauth_client.unlinked` audit row and returns `204`; responds `404` when the link does not exist.","operationId":"PortalSsoConnectionsController_unlinkOAuthClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"oauthClientId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Remove the link; that client stops routing to this connection."},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"This OAuth client is not linked to the connection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"This OAuth client is not linked to the connection.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/oauth-clients/{oauthClientId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Unlink an OAuth client from a connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments":{"get":{"description":"Lists the Environment bindings for an `end_user` SSO connection — the publishable-key / Direct-API analogue of OAuth-client links — ordered oldest-first, each carrying per-Environment placement config. Returns `404` when the connection is not found or is not `end_user`-scope. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_listEnvironmentLinks","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The Environments this connection is bound to for publishable-key (Direct-API) SSO, each with its per-Environment placement config.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/EnvironmentSsoLinkResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List Environment bindings for an end-user connection","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments/{environmentId}":{"post":{"description":"Binds an `end_user` SSO connection to an Environment in the Account for publishable-key (Direct-API) sign-in, storing the per-Environment placement config from the body. Every referenced placement node must belong to that Environment and the `default_role_id` must be an active, non-system role of it (`400` on mismatch), and setting `is_default` clears any other default for the same Environment in the same transaction. Returns `400` if the connection is not `end_user`-scope, `404` if the connection or Environment is not in the Account, emits an `sso.connection.environment.linked` audit row, and returns `201`.","operationId":"PortalSsoConnectionsController_linkEnvironment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"environmentId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkEnvironmentSsoDto"}}}},"responses":{"201":{"description":"Binds the connection to an Environment for publishable-key SSO, with per-Environment hierarchy-placement config. At most one default per Environment.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentSsoLinkResponseDto"}}}}}},"400":{"description":"Only end-user SSO connections can be linked to OAuth clients.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Only end-user SSO connections can be linked to OAuth clients.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments/{environmentId}","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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments/{environmentId}","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Bind an end-user connection to an Environment","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates the placement config of an existing Environment binding; only fields present in the body are changed, and placement nodes/role are re-validated against the Environment (`400` on mismatch). Switching `is_default` to true clears any other default for the same Environment in the same transaction. Emits an `sso.connection.environment.updated` audit row; responds `404` when the binding does not exist.","operationId":"PortalSsoConnectionsController_updateEnvironmentLink","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"environmentId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentSsoLinkDto"}}}},"responses":{"200":{"description":"Update the binding's default flag and/or placement config.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentSsoLinkResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"This Environment is not bound to the connection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"This Environment is not bound to the connection.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments/{environmentId}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an Environment binding","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Removes the binding between an `end_user` SSO connection and an Environment, leaving both records intact. Emits an `sso.connection.environment.unlinked` audit row and returns `204`; responds `404` when the binding does not exist.","operationId":"PortalSsoConnectionsController_unlinkEnvironment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"environmentId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Remove the binding; that Environment stops routing to this connection."},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"This Environment is not bound to the connection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"This Environment is not bound to the connection.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/environments/{environmentId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Unbind an end-user connection from an Environment","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/organizations":{"get":{"description":"Lists every organization this `end_user` connection is bound to across the Account's Environments, oldest first, each with the organization's Environment and the membership role a just-in-time login receives. Returns `404` when the connection is not found or is not `end_user`-scope. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_listOrganizationBindings","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The organizations (across the Account's Environments) whose logins this connection lands in.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SsoConnectionOrganizationResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/organizations","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the organizations an end-user connection is bound to","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/recent-logins":{"get":{"description":"Returns up to the 100 most recent login attempts for a connection, newest first, read from the Account's audit log (`sso.login.succeeded` / `sso.login.failed` rows matched on `connection_id`). Responds `404` when the connection is not found in the Account. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_recentLogins","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The last 100 login attempts for the connection (newest first), projected from the audit log — outcome, identity email, failure reason, IP, timestamp.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SsoRecentLoginDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No SSO connection with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SSO connection with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/connections/{id}/recent-logins","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List a connection's recent login attempts","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/oauth-clients/{oauthClientId}/links":{"get":{"description":"Lists every SSO-connection link attached to a given OAuth client (the reverse of listing links by connection), ordered oldest-first. Responds `404` when the OAuth client is not in the Account. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_listClientLinks","parameters":[{"name":"oauthClientId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Every end-user SSO connection linked to the OAuth client, each with its per-client placement config. Resolve connection names client-side via the connections list.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/OAuthClientLinkResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No OAuth client with that id in this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No OAuth client with that id in this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/oauth-clients/{oauthClientId}/links","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List an OAuth client's SSO connection links","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/sign-in-coverage":{"get":{"description":"Returns the distinct Environment ids that have a usable end-user SSO sign-in path — every Environment owning an active OAuth client that is linked to an `active`, `end_user` SSO connection in the Account. Drives the dashboard \"no usable sign-in method\" banner, so active-but-unlinked connections and inactive clients do not count. The response is an unpaginated `{ items }` array of id strings.","operationId":"PortalSsoConnectionsController_signInCoverage","parameters":[],"responses":{"200":{"description":"The ids of every Environment that has at least one OAuth client linked to an active end-user SSO connection. Lets the dashboard tell, per Environment, whether SSO is a usable sign-in method when password and email-OTP login are both off.","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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List Environments reachable by SSO","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/domains":{"get":{"description":"Lists all admin-SSO domain claims for the Account with their verification status (`pending`, `verified`, or `failed`). Used to drive admin-SSO email-domain routing. The response is an unpaginated `{ items }` array.","operationId":"PortalSsoConnectionsController_listDomains","parameters":[],"responses":{"200":{"description":"All domains this Account is claiming / has verified for admin-SSO routing, with their DNS TXT record and verification status.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SsoDomainResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List SSO domain claims","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Claims an email domain for admin-SSO routing and issues a DNS TXT challenge (a `canopy-domain-verification=` token published under a `_canopy-verification` subdomain). The domain is normalized from a pasted URL or host and rejected (`400`) if it is malformed or a known public email provider. Idempotent: re-claiming an existing (Account, domain) returns the existing row and token rather than minting a new challenge. Emits an `sso.domain.claimed` audit row and returns `201`.","operationId":"PortalSsoConnectionsController_claimDomain","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimSsoDomainDto"}}}},"responses":{"201":{"description":"Register a domain and get the DNS TXT record to publish. Idempotent — re-claiming returns the existing challenge. Verify it before it can route logins.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoDomainResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Claim a domain for admin SSO","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}/verify":{"post":{"description":"Resolves the domain's DNS TXT record and, if it carries this claim's token, flips the claim to `verified`; a verified domain is exclusive to one Account (partial unique index). Returns `400` when the record is missing or the token does not match (the claim is marked `failed`), `404` when no claim exists, and `409` when another Account already verified the domain. Rate-limited to 10 requests per minute; emits `sso.domain.verified` or `sso.domain.verification_failed` audit rows.","operationId":"PortalSsoConnectionsController_verifyDomain","parameters":[{"name":"domain","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Resolve the domain's DNS TXT record and, if it carries this claim's token, mark it verified. 400 if the record isn't found yet; 409 if another Account already verified the domain.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SsoDomainResponseDto"}}}}}},"400":{"description":"The expected DNS TXT record was not found for this domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The expected DNS TXT record was not found for this domain.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}/verify","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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No such domain claim on this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No such domain claim on this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}/verify","method":"POST"}}}}},"409":{"description":"This domain is already verified by another Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"This domain is already verified by another Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}/verify","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Verify a claimed domain","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}":{"delete":{"description":"Removes a domain claim from the Account; any connection routing on that domain stops matching it on the next login. Emits an `sso.domain.removed` audit row and returns `204`; responds `404` when no claim with that domain exists in the Account.","operationId":"PortalSsoConnectionsController_removeDomain","parameters":[{"name":"domain","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Delete a domain claim. Connections listing it stop routing for that domain on the 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":"/portal/v1/accounts/{accountSlug}/sso/connections","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":"/portal/v1/accounts/{accountSlug}/sso/connections","method":"GET"}}}}},"404":{"description":"No such domain claim on this Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No such domain claim on this Account.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/sso/domains/{domain}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove a domain claim","tags":["portal-sso-connections"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities":{"get":{"description":"Paginated listing of the Account's end-user identity directory, spanning every `Environment` in the Account rather than one App/Environment. Supports a free-text `q` search and filters for `status`, `has_any_assignment`, `environment_id`, and `not_in_env` (used by the add-from-directory picker to exclude identities already in a given Environment); `sort` accepts `name`, `email`, or `created`, defaulting to `first_name` ASC. Includes SSO/SCIM-provisioned, invited-but-unaccepted, and orphaned (zero-membership) identities.","operationId":"PortalAccountIdentitiesController_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"]}},{"name":"status","required":false,"in":"query","description":"Filter by the Account-wide `is_active` flag. Omit to show all.","schema":{"type":"string","enum":["active","inactive"]}},{"name":"has_any_assignment","required":false,"in":"query","description":"When true, only identities with at least one role assignment somewhere in the Account. When false, only identities with zero assignments (the orphan filter — surfaces identities that exist in the directory but haven't been given access anywhere). Omit to show all.","schema":{"type":"boolean"}},{"name":"not_in_env","required":false,"in":"query","description":"Exclude identities that already have an active EnvironmentMembership for the given Environment id. Drives the 'Add from directory' picker — the candidate list should only contain identities NOT already in the target Environment.","schema":{"type":"string"}},{"name":"environment_id","required":false,"in":"query","description":"Filter the directory by Environment. Pass an Environment id to show only identities with an active membership there, or the literal `none` to show directory-only identities (no membership anywhere). Omit to show all.","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/AccountIdentityRowDto"}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identities in Account","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates an identity at the Account tier. When `environment_id` is supplied, a same-transaction `EnvironmentMembership` is created so the identity can sign into that Environment immediately; when omitted, the identity is created as an orphan in the directory and access is granted later via the membership endpoints. The Environment must belong to this Account (`404`). Returns `409` on a duplicate email and `400` when the supplied password is found in the breach list.","operationId":"PortalAccountIdentitiesController_createIdentity","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountIdentityDto"}}}},"responses":{"201":{"description":"Creates an identity at the Account tier. When `environment_id` is provided, a same-request EnvironmentMembership row is also created so the identity can sign into that Environment immediately. When `environment_id` is omitted, the identity is created as an orphan in the directory — the admin can grant access to one or more Environments later via the membership endpoints. Distinct from the env-scoped identity create which is scoped via the URL.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityDetailResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities","method":"GET"}}}}},"404":{"description":"Environment not found in this Account — cannot attach an identity to an Environment outside its own Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Account — cannot attach an identity to an Environment outside its own Account","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identities","method":"POST"}}}}},"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":"/portal/v1/accounts/{accountSlug}/identities","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create an Account identity","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/bulk-create":{"post":{"description":"Partial-success bulk create for the directory CSV-import wizard, capped at 200 rows per request. Each row is processed independently so per-row validation, duplicate-email, and Environment-not-in-Account errors fold into the failures array rather than aborting the batch; an optional per-row `environment_id` provisions an `EnvironmentMembership` in the same transaction. Returns `200` on full success or `207 Multi-Status` when any row failed, with a `{ summary, results }` body either way.","operationId":"PortalAccountIdentitiesController_bulkCreateIdentities","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateAccountIdentitiesDto"}}}},"responses":{"200":{"description":"Partial-success bulk-create for the Tenant > Identities CSV import wizard. Each row is processed independently; per-row business errors fold into the failures array. Cap of 200 rows per request — the wizard batches the CSV at the same threshold so a single upload becomes N requests. Returns 200 on full success or 207 Multi-Status when any row failed; the body is always `{ summary, results }` so callers iterate the same way regardless. Optional `environment_id` per row grants the identity access to that Environment on creation (creates an EnvironmentMembership in the same transaction). — 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/AccountIdentityDetailResponseDto"}}}}}}}}},"207":{"description":"Partial-success bulk-create for the Tenant > Identities CSV import wizard. Each row is processed independently; per-row business errors fold into the failures array. Cap of 200 rows per request — the wizard batches the CSV at the same threshold so a single upload becomes N requests. Returns 200 on full success or 207 Multi-Status when any row failed; the body is always `{ summary, results }` so callers iterate the same way regardless. Optional `environment_id` per row grants the identity access to that Environment on creation (creates an EnvironmentMembership in the same transaction). — 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/AccountIdentityDetailResponseDto"}}},{"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk-create Account identities","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/summary":{"get":{"description":"Returns aggregate counts for the Account's identity directory — total, active, inactive, email-unverified, and orphaned (zero `EnvironmentMembership`). Account-scoped across all Environments. Drives the stat cards on the directory page.","operationId":"PortalAccountIdentitiesController_getDirectorySummary","parameters":[],"responses":{"200":{"description":"Aggregate counts of identities in the Account — total, active, inactive, email-unverified, and orphaned (zero EnvironmentMemberships). Drives the stat card on the Tenant > Identities page.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentitiesSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get directory counts for the Account","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}":{"get":{"description":"Returns the full detail record for one identity by `id`, scoped to this Account. Includes profile, status, and `EnvironmentMembership` / assignment context across the Account's Environments. Returns `404` for an unknown id or one owned by another Account (indistinguishable on the wire).","operationId":"PortalAccountIdentitiesController_getIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Full identity profile + active EnvironmentMembership rows + per-env assignment counts. The aggregate view that drives the Tenant > Identities detail drawer.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityDetailResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an Account identity","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates the editable profile fields of one Account identity (name, metadata) by `id`. Account-scoped; does not touch status, credentials, or `EnvironmentMembership` rows. Returns `404` when the identity is not found in this Account.","operationId":"PortalAccountIdentitiesController_updateIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAccountIdentityDto"}}}},"responses":{"200":{"description":"Updates first_name / last_name / metadata. Email and external_id are updated through a separate flow (email changes require re-verification, external_id is owned by SSO/SCIM when those features ship).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityDetailResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an Account identity profile","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/status":{"patch":{"description":"Activates or deactivates one identity via the `is_active` body flag — a reversible lifecycle toggle that retains all data (profile, memberships, assignments), distinct from erasure. Rejects a no-op with `409` when the identity is already in the requested state, keeping the audit signal clean, and writes an `identity.status_set` row. Returns `404` when the identity is not found in this Account.","operationId":"PortalAccountIdentitiesController_setStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAccountIdentityStatusDto"}}}},"responses":{"200":{"description":"Flips the Identity row's `is_active` master kill switch. Deactivation is Account-wide and revokes access in every Environment; reactivation restores the row but the dual-gate still requires an active EnvironmentMembership for each Environment. Writes Account-tier audit row.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityDetailResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/status","method":"PATCH"}}}}},"409":{"description":"Identity is already in the requested state (no-op rejected to keep audit signal clean)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Identity is already in the requested state (no-op rejected to keep audit signal clean)","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identities/{id}/status","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Set Account-wide is_active flag","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/erase":{"post":{"description":"Irreversible GDPR/CCPA erasure (\"right to be forgotten\") for one identity. In a single transaction it overwrites every direct identifier (email becomes a non-routable `.invalid` tombstone, name/avatar/external id/metadata cleared), destroys all credentials and MFA factors/recovery codes/trusted devices, severs federation, revokes every session and pending invite, deactivates all `EnvironmentMemberships`, drops all role assignments, and anonymizes historical audit rows that reference the identity. Idempotent — erasing an already-erased identity is a no-op — and returns `204`.","operationId":"PortalAccountIdentitiesController_eraseIdentity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Irreversibly wipes the identity's personal data — email, name, IdP subject, credentials, MFA secrets — revokes all sessions and pending invites, and removes access in every Environment. Historical audit rows referencing the identity are anonymized in place (label snapshots + email-bearing metadata removed) while keeping the event and id. A pseudonymous tombstone row remains so audit history keeps its references. Distinct from deactivation (`is_active = false`), which retains the data and is reversible. Idempotent; writes an Account-tier audit row carrying only the identity id."},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/erase","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Erase an identity (GDPR/CCPA right to be forgotten)","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/reset-password":{"post":{"description":"Triggers an admin-initiated password reset for one identity: mints a hashed, time-limited reset token and emails the identity a link into the end-user reset flow. Account-scoped; returns `404` when the identity is not found and is rejected for an already-erased identity. Writes an `identity.password_reset_triggered` audit row and returns a confirmation message.","operationId":"PortalAccountIdentitiesController_resetPassword","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Generates a single-use reset token and emails the identity. The admin never sees the token. Distinct from the password_reset invite intent — that flow rotates a password and creates an audit trail of admin-driven recovery; this flow just initiates the self-service reset on the identity's behalf.","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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/reset-password","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Admin-trigger a password reset email","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/resend-verification":{"post":{"description":"Re-sends the email-verification message to one identity whose email is not yet verified. Account-scoped; returns `404` when the identity is not found and `409` when the email is already verified (nothing to resend). Returns a confirmation message.","operationId":"PortalAccountIdentitiesController_resendVerification","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Regenerates the email-verification token and re-sends the verification email. Useful when the original verification email expired before the identity acted on it.","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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/resend-verification","method":"POST"}}}}},"409":{"description":"Identity email is already verified — nothing to resend","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Identity email is already verified — nothing to resend","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identities/{id}/resend-verification","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Re-send email verification","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/revoke-sessions":{"post":{"description":"Revokes every active session / refresh token for one identity across all Environments, forcing re-authentication everywhere — the primary incident-response action for a suspected-compromised account. Does not deactivate or delete the identity; profile, memberships, and assignments are untouched. An optional `reason` is recorded on the `session.all_revoked` audit row, and the count of revoked sessions is returned. Returns `404` when the identity is not found.","operationId":"PortalAccountIdentitiesController_revokeSessions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeSessionsDto"}}}},"responses":{"200":{"description":"Invalidates every active refresh token / session for the target identity across all environments, forcing re-authentication everywhere. The primary incident-response action when an account is suspected compromised (e.g. phishing). Does NOT deactivate or delete the identity — profile, EnvironmentMemberships, and role assignments are untouched; the user simply gets logged out everywhere. Optional `reason` is recorded on the `session.all_revoked` audit row. Returns the number of sessions revoked.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RevokeSessionsResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/revoke-sessions","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke all active sessions for an identity","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/audit":{"get":{"description":"Paginated, newest-first audit feed for a single identity, filtered to `resource_type='identity'` and the given `id` within this Account. Drives the activity section of the identity detail view. Returns `404` when the identity is not found in this Account.","operationId":"PortalAccountIdentitiesController_listIdentityAudit","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/AuditLogRowDto"}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/audit","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List audit events for an Account identity","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/environment-memberships":{"post":{"description":"Creates an active `EnvironmentMembership` attaching the Account-level identity to the named Environment, granting it sign-in access there. The Environment must belong to the same Account as the identity (`404`). Returns `409` when the identity is already an active member, `404` when the identity is unknown, and writes an env-tier audit row.","operationId":"PortalAccountIdentitiesController_addEnvMembership","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddEnvMembershipDto"}}}},"responses":{"201":{"description":"Creates an active EnvironmentMembership row attaching the Account-level identity to the named Environment. The Environment must belong to the same Account as the identity. Writes an env-tier audit row.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvMembershipResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/environment-memberships","method":"POST"}}}}},"409":{"description":"Identity is already an active member of this Environment — call remove first to revoke, or no-op","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Identity is already an active member of this Environment — call remove first to revoke, or no-op","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identities/{id}/environment-memberships","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Add an identity to an Environment (create EnvironmentMembership)","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/environment-memberships/bulk-attach":{"post":{"description":"Partial-success bulk attach for the add-from-directory picker: creates an `EnvironmentMembership` in one target Environment for each identity in `identity_ids`. Each identity is processed independently so pre-existing memberships and validation errors fail individually without aborting the batch. Returns `200` on full success or `207 Multi-Status` on mixed outcomes, with a `{ summary, results }` body either way. Grants membership only — assigning a role at a node is a separate env-scoped call.","operationId":"PortalAccountIdentitiesController_bulkAttachEnvMemberships","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAttachEnvMembershipsDto"}}}},"responses":{"200":{"description":"Per-row EnvironmentMembership creation. Each identity is processed independently — pre-existing memberships 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. Membership only — to assign a role at a node, follow up with the env-scoped bulk-create-assignments endpoint. — 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/EnvMembershipResponseDto"}}}}}}}}},"207":{"description":"Per-row EnvironmentMembership creation. Each identity is processed independently — pre-existing memberships 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. Membership only — to assign a role at a node, follow up with the env-scoped bulk-create-assignments endpoint. — 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/EnvMembershipResponseDto"}}},{"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk-attach EnvironmentMemberships for the Add-from-directory picker","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/identities/{id}/environment-memberships/{envId}":{"delete":{"description":"Deactivates the `EnvironmentMembership` joining one identity to the Environment identified by `envId` and revokes every role assignment that identity holds at the Environment's hierarchy nodes. The identity row itself is retained (it may still belong to other Environments). Returns `404` when the identity is unknown or holds no membership in that Environment, and responds `204`.","operationId":"PortalAccountIdentitiesController_removeEnvMembership","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"envId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Deactivates the EnvironmentMembership row and revokes every role assignment the identity holds at this Environment's hierarchy nodes. The Identity row itself stays (it may still belong to other Environments)."},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identities/{id}/environment-memberships/{envId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove an identity from an Environment (revoke EnvironmentMembership)","tags":["portal-account-identities"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityMfaController_getSummary","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get MFA enrollment summary for an identity","tags":["portal-account-identity-mfa"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityMfaController_forceReset","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"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.","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa","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":"/portal/v1/accounts/{accountSlug}/identities/{id}/mfa/reset","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Force-reset an identity's MFA — admin recovery action","tags":["portal-account-identity-mfa"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityInvitesController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List invites","tags":["portal-account-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates an Account-tier identity invite and sends the invitation email. Unlike the env-scoped invite, it carries no `role_id` / `node_id`; the required `environment_id` targets an Environment so acceptance creates an `EnvironmentMembership` there, and the email link uses that Environment's OAuth client `invite_redirect_url` when set. The Environment must belong to this Account (`404`), and a duplicate pending invite for the same email at this scope returns `409`.","operationId":"PortalAccountIdentityInvitesController_createInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteAccountIdentityDto"}}}},"responses":{"201":{"description":"Creates an Account-tier identity invite and sends the email. Distinct from the env-scoped invite endpoint — this one does not carry `role_id` / `node_id`. The required `environment_id` targets an Environment: acceptance creates an EnvironmentMembership for that Environment and the email link uses the Environment's OAuth client's `invite_redirect_url` when present.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccountIdentityInviteResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identity-invites","method":"GET"}}}}},"404":{"description":"Environment not found in this Account — cannot attach an identity to an Environment outside its own Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Account — cannot attach an identity to an Environment outside its own Account","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identity-invites","method":"POST"}}}}},"409":{"description":"A pending invite already exists for this email at this scope. Revoke the existing invite or wait for it to expire before issuing a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"A pending invite already exists for this email at this scope. Revoke the existing invite or wait for it to expire before issuing a new one.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/identity-invites","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Invite an identity at the Account tier","tags":["portal-account-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityInvitesController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get invite summary","tags":["portal-account-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"identities.view","description":"View end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityInvitesController_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/MessageResponseDto"}}}}}},"400":{"description":"Only pending or expired invites can be resent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Only pending or expired invites can be resent","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identity-invites/{id}/resend","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Resend an invite","tags":["portal-account-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountIdentityInvitesController_revokeInvite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Invite revoked"},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/identity-invites/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke an invite","tags":["portal-account-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"identities.manage","description":"Manage end-user identities across the account","category":"Identities"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/members":{"get":{"description":"Returns every `AccountMember` (the human admins/owners of the Account), as an unpaginated `{ items }` list. Account-scoped and authorized by account membership; members have access across every Application in the Account. Adding members happens through the user-invite flow, not here.","operationId":"PortalAccountMembersController_listMembers","parameters":[],"responses":{"200":{"description":"AccountMembers returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AccountMemberResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/members","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":"/portal/v1/accounts/{accountSlug}/members","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List platform-admin members of the Account","tags":["portal-account-members"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/members/{memberId}":{"delete":{"description":"Removes one `AccountMember` by `memberId` and immediately revokes their outstanding access tokens so they lose access before token expiry. Self-removal is rejected (`400`), owners are non-removable (`403`) so ownership can never be orphaned, and an unknown member returns `404`. Writes an `account_member.removed` audit row and responds `204`.","operationId":"PortalAccountMembersController_removeMember","parameters":[{"name":"memberId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Member 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":"/portal/v1/accounts/{accountSlug}/members","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":"/portal/v1/accounts/{accountSlug}/members","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Remove a platform admin from the Account","tags":["portal-account-members"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create a webhook subscription","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.manage","description":"Manage account-tier webhook subscriptions","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List webhook subscriptions","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.view","description":"View account-tier webhook subscriptions and deliveries","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/webhooks/summary":{"get":{"description":"Returns active/inactive subscription counts for the Account's webhooks at the account scope, with an optional `q` substring filter on the delivery URL. Requires `webhook.view`.","operationId":"PortalAccountWebhooksController_summary","parameters":[{"name":"q","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook active/inactive summary returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ActiveSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Active/inactive webhook counts for the environment","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.view","description":"View account-tier webhook subscriptions and deliveries","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List subscribable webhook event types","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.view","description":"View account-tier webhook subscriptions and deliveries","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks/{id}/deliveries","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List delivery attempts for a webhook","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.view","description":"View account-tier webhook subscriptions and deliveries","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a webhook subscription","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.view","description":"View account-tier webhook subscriptions and deliveries","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update a webhook subscription","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.manage","description":"Manage account-tier webhook subscriptions","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalAccountWebhooksController_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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete a webhook subscription","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.manage","description":"Manage account-tier webhook subscriptions","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/webhooks/{id}/rotate-secret":{"post":{"description":"Generates a new signing secret for one account-scoped webhook subscription `id`, replacing the old one. The new secret is returned only in this response and is never retrievable again; existing deliveries signed with the prior secret will no longer validate. Returns `404` for an unknown subscription, writes a `webhook.secret_rotated` audit row, and requires `webhook.manage`.","operationId":"PortalAccountWebhooksController_rotateSecret","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook signing secret rotated — the new secret is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSecretRotatedResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/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":"/portal/v1/accounts/{accountSlug}/webhooks/{id}/rotate-secret","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Rotate a webhook subscription's signing secret","tags":["portal-account-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"account_webhooks.manage","description":"Manage account-tier webhook subscriptions","category":"Account"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}":{"get":{"description":"Returns the active Application — resolved from the `:accountSlug`/`:appSlug` path by the guard chain — including its `name`, `slug`, `default_environment_id`, `settings`, and `version`. No identifier is taken in the body since the Application is already bound to the request context.","operationId":"PortalApplicationsController_getApplication","parameters":[],"responses":{"200":{"description":"Application returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApplicationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the current Application","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Patches the active Application's mutable fields (currently `name`); omitted fields are left unchanged. Supply the Application's current `version` in the `If-Match` header for optimistic concurrency — a stale value returns `409` so a concurrent edit is surfaced rather than silently overwritten. Returns the updated Application and emits an `application.updated` audit event.","operationId":"PortalApplicationsController_updateApplication","parameters":[{"name":"if-match","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApplicationDto"}}}},"responses":{"200":{"description":"Application updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApplicationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}},"409":{"description":"Application has been modified since the requested version was read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Application has been modified since the requested version was read","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update the Application name or settings","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes the active Application and cascade-removes its Environments and all their child rows. Refuses with `400` when it is the last Application in the Account, so an admin always has somewhere to land after deletion. Returns `204 No Content` on success, `404` when the Application no longer exists, and emits an `application.deleted` audit event.","operationId":"PortalApplicationsController_deleteApplication","parameters":[],"responses":{"204":{"description":"Application deleted along with every Environment, role, permission, assignment, OAuth client, webhook, identity invite, and audit row scoped under it"},"400":{"description":"Cannot delete the last Application in this Account — an Account must have at least one Application at all times","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Cannot delete the last Application in this Account — an Account must have at least one Application at all times","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Application not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an Application","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/default-environment":{"put":{"description":"Repoints the active Application's `default_environment_id` to the Environment named by `slug` in the body. The default Environment is where an admin lands when navigating to the Application without an explicit `:envSlug`. The target must belong to this Application — a foreign or unknown slug returns `400`. Returns `200` with the updated Application and emits an `application.default_environment_set` audit event.","operationId":"PortalApplicationsController_setDefaultEnvironment","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDefaultEnvironmentDto"}}}},"responses":{"200":{"description":"Application updated — `default_environment_id` repointed to the supplied env","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApplicationResponseDto"}}}}}},"400":{"description":"`default_environment_id` does not point at an Environment in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"`default_environment_id` does not point at an Environment in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/default-environment","method":"PUT"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Set the App's default Environment","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/me/state":{"get":{"description":"Returns the calling admin's resume state for the active Application as `{ environment_slug }`. The slug is resolved from the admin's last-visited Environment when it still exists in this Application, falling back to the Application's `default_environment_id`, and is `null` when neither resolves. Drives where the dashboard lands the admin on sign-in.","operationId":"PortalApplicationsController_getUserState","parameters":[],"responses":{"200":{"description":"Returns the env slug the admin should land on for this App — their last-active env if recorded, else the App's default. Null when the App has no envs.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserAppStateResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the admin's resume-state for this Application","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"put":{"description":"Writes through the calling admin's last-visited Environment for the active Application, given an `environment_slug` in the body, so the dashboard can resume there on the next sign-in. The Environment must belong to this Application — an unknown slug returns `400` rather than poisoning the resume state. Returns `200` echoing the stored `environment_slug`.","operationId":"PortalApplicationsController_setUserState","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserAppStateDto"}}}},"responses":{"200":{"description":"Records the admin's currently-active env so the dashboard can resume them on the same env after sign-out / device switch.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserAppStateResponseDto"}}}}}},"400":{"description":"The supplied `environment_slug` does not exist in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The supplied `environment_slug` does not exist in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/me/state","method":"PUT"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Update the admin's resume-state for this Application","tags":["portal-applications"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream":{"get":{"description":"Server-Sent Events live-tail of newly written admin audit rows (actor is an operator: `user`, `api_key`, `platform`, `system`), scoped to the current Account. Accepts the same filter DSL as the query endpoint; date range and cursor are ignored since every event is current. Each row is delivered in an `event: audit-log` frame. The stream closes on client disconnect or a 30-minute hard timeout, and is subject to a per-Account cap of 10 concurrent streams across both surfaces — exceeding it returns `429`. Writes one `audit.live_tail.started` row at open and one `audit.live_tail.ended` row at close.","operationId":"PortalAuditLogAdminController_liveTail","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","organization","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":"Server-Sent Events stream of new audit rows as they're written. Accepts the same filter DSL as the query endpoint (date range and cursor are ignored — every event is 'now'). Each delivered row is wrapped in an `event: audit-log` frame with the serialized row as `data`. Connection closes on client disconnect, JWT expiry (within the request lifecycle), or a 30-minute hard timeout. Customer-visible rows only, scoped to this surface (admin or identities). Per-Account concurrency cap: 10 simultaneous streams across both surfaces combined. Writes one `audit.live_tail.started` row at open and one `audit.live_tail.ended` row at close."},"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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","method":"GET"}}}}},"429":{"description":"Account has reached the live-tail concurrency limit (10 simultaneous streams across admin + identity surfaces combined). Close an existing stream and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":429,"code":null,"message":"Account has reached the live-tail concurrency limit (10 simultaneous streams across admin + identity surfaces combined). Close an existing stream and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Live-tail admin audit-log events (SSE)","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/admin":{"get":{"description":"Admin surface — returns rows where the actor is an operator (`user`, `api_key`, `platform`, `system`). Admin-on-identity rows (e.g., an admin force-resetting an identity's MFA) appear here AND on the identity surface; same database row, two endpoints by design.","operationId":"PortalAuditLogAdminController_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","organization","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Query admin audit-log events","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/admin/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":"PortalAuditLogAdminController_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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create an admin audit-log export job (CSV / NDJSON)","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalAuditLogAdminController_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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List recent audit-log export jobs","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/admin/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":"PortalAuditLogAdminController_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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/export/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an audit-log export job","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes the rendered file from object storage (if any) and the job record. Idempotent against an already-swept object.","operationId":"PortalAuditLogAdminController_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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/export/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an audit-log export job","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/admin/{id}":{"get":{"description":"Returns a single admin-surface audit row by `id` plus two related-event sidecars: `related_by_correlation` (rows sharing this row's `correlation_id`, chronological, capped at 50) and `related_by_actor` (the same actor's last 10 events within a 1-hour window, newest-first). All reads honor `customer_visible = TRUE` and the admin surface discriminator. Returns `404` — indistinguishably — when the row is missing, belongs to another Account, is off this surface, or is internal-only. Writes one `audit.row.viewed` audit row.","operationId":"PortalAuditLogAdminController_getById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Single audit-log row plus two related-event sidecars: `related_by_correlation` (every other row sharing this row's `correlation_id`, ordered chronologically, capped at 50) and `related_by_actor` (the same actor's last 10 events within a 1-hour window of this row, newest-first). Both sidecars honor `customer_visible = TRUE` AND the surface discriminator independently. Writes one `audit.row.viewed` audit row.","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/admin/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an admin audit-log row with related events","tags":["portal-audit-log-admin"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream":{"get":{"description":"Server-Sent Events live-tail of newly written identity-surface audit rows (an end-user identity is the actor or the resource, so admin-on-identity actions appear here too), scoped to the current Account. Accepts the same filter DSL as the query endpoint; date range and cursor are ignored since every event is current. Each row is delivered in an `event: audit-log` frame. The stream closes on client disconnect or a 30-minute hard timeout, and is subject to a per-Account cap of 10 concurrent streams across both surfaces — exceeding it returns `429`. Writes one `audit.live_tail.started` row at open and one `audit.live_tail.ended` row at close.","operationId":"PortalAuditLogIdentitiesController_liveTail","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","organization","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":"Server-Sent Events stream of new audit rows as they're written. Accepts the same filter DSL as the query endpoint (date range and cursor are ignored — every event is 'now'). Each delivered row is wrapped in an `event: audit-log` frame with the serialized row as `data`. Connection closes on client disconnect, JWT expiry (within the request lifecycle), or a 30-minute hard timeout. Customer-visible rows only, scoped to this surface (admin or identities). Per-Account concurrency cap: 10 simultaneous streams across both surfaces combined. Writes one `audit.live_tail.started` row at open and one `audit.live_tail.ended` row at close."},"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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","method":"GET"}}}}},"429":{"description":"Account has reached the live-tail concurrency limit (10 simultaneous streams across admin + identity surfaces combined). Close an existing stream and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":429,"code":null,"message":"Account has reached the live-tail concurrency limit (10 simultaneous streams across admin + identity surfaces combined). Close an existing stream and retry.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Live-tail identity audit-log events (SSE)","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/identities":{"get":{"description":"Identity surface — returns rows where an end-user identity is the actor OR the resource. Admin-on-identity rows appear here so customers can answer 'everything that ever happened to identity X' from a single endpoint. Same database row may also be returned by the admin surface.","operationId":"PortalAuditLogIdentitiesController_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","organization","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Query identity audit-log events","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/identities/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":"PortalAuditLogIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create an identity audit-log export job (CSV / NDJSON)","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalAuditLogIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List recent audit-log export jobs","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/identities/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":"PortalAuditLogIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/export/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an audit-log export job","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes the rendered file from object storage (if any) and the job record. Idempotent against an already-swept object.","operationId":"PortalAuditLogIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/export/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an audit-log export job","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/audit-log/identities/{id}":{"get":{"description":"Returns a single identity-surface audit row by `id` plus two related-event sidecars: `related_by_correlation` (rows sharing this row's `correlation_id`, chronological, capped at 50) and `related_by_actor` (the same actor's last 10 events within a 1-hour window, newest-first). All reads honor `customer_visible = TRUE` and the identity surface discriminator. Returns `404` — indistinguishably — when the row is missing, belongs to another Account, is off this surface, or is internal-only. Writes one `audit.row.viewed` audit row.","operationId":"PortalAuditLogIdentitiesController_getById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Single audit-log row plus two related-event sidecars: `related_by_correlation` (every other row sharing this row's `correlation_id`, ordered chronologically, capped at 50) and `related_by_actor` (the same actor's last 10 events within a 1-hour window of this row, newest-first). Both sidecars honor `customer_visible = TRUE` AND the surface discriminator independently. Writes one `audit.row.viewed` audit row.","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/stream","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":"/portal/v1/accounts/{accountSlug}/audit-log/identities/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an identity audit-log row with related events","tags":["portal-audit-log-identities"],"x-canopy-required-authorization":{"capability":{"key":"audit.view","description":"View the account-wide audit log","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments":{"get":{"description":"Returns every Environment in the active Application as an `{ items }` collection, ordered as stored. Each row carries the Environment's `name`, `slug`, non-secret `publishable_key`, and `settings` (access model, auth toggles, MFA, CORS allowlist). The Application is resolved from the path (`:accountSlug`/`:appSlug`) and the guard chain; no pagination is applied since an Application holds only a handful of Environments.","operationId":"PortalEnvironmentsController_list","parameters":[],"responses":{"200":{"description":"Environments returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/EnvironmentResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List Environments in an Application","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates a new Environment in the active Application from a `name` and optional `slug` (derived from the name when omitted), returning `201` with the Environment plus one-time `oauth_client_rotations` and `webhook_subscription_rotations` receipts. When `copy_from` names an existing Environment slug, that source's permissions, roles, role-permission joins, hierarchy nodes, OAuth client configs, and webhook configs are cloned — with freshly minted publishable key, OAuth client ids/secrets, and webhook signing secrets, since secrets never cross between Environments; identities, memberships, assignments, audit logs, and deliveries are not copied. A duplicate `slug` within the Application returns `409` and an `environment.created` audit event is emitted.","operationId":"PortalEnvironmentsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentDto"}}}},"responses":{"201":{"description":"Environment created. When `copy_from` is set, permissions/roles/role-permission joins/hierarchy nodes/OAuth client configs (without secrets)/webhook configs (without secrets) are cloned from the source env. Identities and role assignments are NOT copied.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentCloneResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"409":{"description":"An Environment with this slug already exists in the Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"An Environment with this slug already exists in the Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create a new Environment in an Application","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}":{"get":{"description":"Returns a single Environment in the active Application resolved by the `:envSlug` path parameter. The response includes the Environment's `name`, `slug`, `publishable_key`, and `settings`. Returns `404` when no Environment with that slug exists in the Application.","operationId":"PortalEnvironmentsController_getOne","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Environment returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a single Environment by slug","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates an Environment's lifecycle fields, identified by `:envSlug`: `name`, `slug`, and the one-way `is_production` promotion. Omitted fields are left unchanged. Sign-in settings are a separate authority and live on the `auth-settings` endpoint. Re-slugging to a value already used by another Environment in the Application returns `409`, and a missing slug returns `404`. Emits an `environment.updated` audit event.","operationId":"PortalEnvironmentsController_update","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentDto"}}}},"responses":{"200":{"description":"Environment updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}","method":"PATCH"}}}}},"409":{"description":"An Environment with this slug already exists in the Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"An Environment with this slug already exists in the Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Rename or re-slug an Environment","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes the Environment named by `:envSlug` and cascade-removes its env-scoped child rows (roles, permissions, nodes, assignments, OAuth clients, refresh tokens, webhook subscriptions, and invites). Refuses with `400` when it is the Application's last Environment or its current `default_environment_id` — repoint the default first in the latter case. Returns `204 No Content` on success, `404` when the slug does not exist, and emits an `environment.deleted` audit event.","operationId":"PortalEnvironmentsController_delete","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted"},"400":{"description":"Cannot delete the last Environment in this Application — an App must have at least one Environment at all times. Cannot delete the App's `default_environment_id` until the customer points it elsewhere.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Cannot delete the last Environment in this Application — an App must have at least one Environment at all times. Cannot delete the App's `default_environment_id` until the customer points it elsewhere.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an Environment","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-settings":{"get":{"description":"Returns the sign-in settings of the Environment named by `:envSlug`: MFA enforcement (hydrated with platform defaults), self-signup and CAPTCHA controls, login-method switches, token lifetimes, allowed browser origins, and SSO return URLs. Keys the customer never set are omitted except the hydrated MFA bundle. Returns `404` when the slug does not exist.","operationId":"PortalEnvironmentsController_getAuthSettings","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Sign-in settings returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentAuthSettingsResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-settings","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an Environment's sign-in settings","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"auth_settings.view","description":"View Environment sign-in settings","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates the sign-in settings of the Environment named by `:envSlug`, applying any supplied fields (`mfa`, `allow_self_signup`, `self_signup_require_captcha`, `login_require_captcha`, `password_login_enabled`, `email_otp_login_enabled`, `self_signup_daily_cap`, `access_token_ttl_seconds`, `refresh_token_ttl_days`, `allowed_web_origins`, `sso_return_urls`); omitted fields are left unchanged and `allowed_web_origins` are normalized and de-duplicated on save. Returns `404` when the slug does not exist. Emits an `environment.auth_settings_updated` audit event recording which keys changed.","operationId":"PortalEnvironmentsController_updateAuthSettings","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentAuthSettingsDto"}}}},"responses":{"200":{"description":"Sign-in settings updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentAuthSettingsResponseDto"}}}}}},"400":{"description":"A setting is out of range or malformed, or the passkey relying party is incomplete — `webauthn_rp_id` and `webauthn_origins` must be set together, and every origin must be on the id or a subdomain of it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"A setting is out of range or malformed, or the passkey relying party is incomplete — `webauthn_rp_id` and `webauthn_origins` must be set together, and every origin must be on the id or a subdomain of it","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-settings","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-settings","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Change an Environment's sign-in settings","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"auth_settings.manage","description":"Change Environment sign-in settings","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/export":{"get":{"description":"Returns the Environment's configuration (resolved by `:envSlug`) as a versioned JSON document: `format_version`, `source_env`, `access_model`, `hierarchy_schema`, and the env's permissions, roles with their permission keys, hierarchy nodes (ref-linked), OAuth client configs, and webhook configs. Secrets are deliberately omitted — OAuth client secrets and webhook signing secrets are not included. The export is read-only and does not modify the Environment; returns `404` when the slug does not exist.","operationId":"PortalEnvironmentsController_export","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Versioned JSON dump: permissions, roles, role-permission joins, hierarchy nodes, OAuth client configs (without secrets), webhook configs (without secrets). Identities, EnvironmentMemberships, role assignments, and audit logs are NOT included.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentExportDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/export","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Export an Environment's configuration as JSON","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/import":{"post":{"description":"Replaces the target Environment's configuration (resolved by `:envSlug`) with the supplied export document. This is destructive: existing roles, permissions, nodes, OAuth clients, webhook subscriptions, and pending invites in the target are wiped before the payload is applied within a single transaction; identities, memberships, and assignments survive except where cascading role/node deletes remove dangling rows. The payload must declare a supported `format_version` (`400` otherwise) and contain exactly one root node, with duplicate keys, cyclic parents, orphaned nodes, and unknown permission references each rejected as `400`. Returns `200` with one-time OAuth client and webhook signing-secret rotation receipts and emits an `environment.imported` audit event; `404` when the slug does not exist.","operationId":"PortalEnvironmentsController_import","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentImportDto"}}}},"responses":{"200":{"description":"Existing roles/permissions/nodes/OAuth clients/webhook subscriptions in the target env are deleted before the payload is applied. Operational state (identities, EnvironmentMemberships, assignments) survives or cascade-deletes via FK rules.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentCloneResponseDto"}}}}}},"400":{"description":"Payload `format_version` is not supported by this server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Payload `format_version` is not supported by this server","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/import","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/import","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Replace an Environment's configuration from a JSON payload (destructive)","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/access-model":{"get":{"description":"Returns the access-control model of the Environment named by `:envSlug` as `{ model, organizations_enabled }`, where `model` is `flat` or `hierarchy` and `organizations_enabled` says whether the organizations container is on. Both are read from the Environment's `settings`, defaulting to `flat` and `false` when unset. Each Environment carries them independently, so siblings in the same Application can differ; returns `404` when the slug does not exist.","operationId":"PortalEnvironmentsController_getAccessModel","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Access model returned (`flat` or `hierarchy`) with `organizations_enabled`","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccessModelResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/access-model","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the Environment's access model","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"put":{"description":"Switches the access-control model of the Environment named by `:envSlug` to the requested `model` (`flat` or `hierarchy`). Setting `flat → hierarchy` flips the model but leaves the schema undefined until a subsequent hierarchy-schema update; the reverse direction is rejected with `400` because flattening has side effects and must go through the dedicated revert-to-flat endpoint. A no-op when the model is unchanged; returns `404` for a missing slug and emits an `environment.access_model_set` audit event.","operationId":"PortalEnvironmentsController_setAccessModel","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAccessModelDto"}}}},"responses":{"200":{"description":"Access model updated. Use revert-to-flat for `hierarchy → flat` — that path runs the flatten side effects (assignments rebased, nodes deleted) and emits its own audit row.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentResponseDto"}}}}}},"400":{"description":"Direct hierarchy → flat is rejected; call revert-to-flat instead","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Direct hierarchy → flat is rejected; call revert-to-flat instead","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/access-model","method":"PUT"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/access-model","method":"PUT"}}}}}},"security":[{"JWT":[]}],"summary":"Switch the Environment's access model","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations-feature":{"put":{"description":"Switches the organizations container of the Environment named by `:envSlug` on or off with `{ enabled }`. The container is independent of the access model: a flat Environment holds one membership role per identity per organization, a hierarchy Environment hangs its tree beneath each organization. Enabling reserves the tier directly under the root for organizations, so it returns `409` while nodes already sit there (revert to flat first) and `400` while a hierarchy schema is not rooted at `organization`. Disabling returns `409` while organizations remain (delete them first) or while a hierarchy schema is configured (revert to flat first). A no-op when the setting is unchanged; `404` for a missing slug. Emits `environment.organizations_enabled` or `environment.organizations_disabled`.","operationId":"PortalEnvironmentsController_setOrganizationsFeature","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOrganizationsFeatureDto"}}}},"responses":{"200":{"description":"Organizations setting updated. Enabling reserves the tier under the root for organizations; disabling requires an empty container and a flat Environment.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/EnvironmentResponseDto"}}}}}},"400":{"description":"Enable refused while the hierarchy schema is not rooted at \"organization\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Enable refused while the hierarchy schema is not rooted at \"organization\"","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations-feature","method":"PUT"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations-feature","method":"PUT"}}}}},"409":{"description":"Enable refused while nodes sit directly under the root; disable refused while organizations or a hierarchy schema remain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Enable refused while nodes sit directly under the root; disable refused while organizations or a hierarchy schema remain","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations-feature","method":"PUT"}}}}}},"security":[{"JWT":[]}],"summary":"Switch the Environment's organizations container on or off","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalEnvironmentsController_getHierarchySchema","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/hierarchy-schema","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get hierarchy schema for the active Environment","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalEnvironmentsController_updateHierarchySchema","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}},{"name":"if-match","required":true,"in":"header","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/hierarchy-schema","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/hierarchy-schema","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update hierarchy schema for the active Environment","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/hierarchy-schema/node-types-in-use":{"get":{"description":"Returns the distinct hierarchy node types that currently have at least one node in the Environment. The dashboard's Manage Levels editor uses this to disable renaming or removing a level whose type is still populated — the same rule the schema-update endpoint enforces server-side.","operationId":"PortalEnvironmentsController_getNodeTypesInUse","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"In-use node types 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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/hierarchy-schema/node-types-in-use","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List hierarchy node types with existing nodes","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/revert-to-flat":{"post":{"description":"Collapses the hierarchy-mode Environment named by `:envSlug` back to flat. Without organizations: surviving assignments are rebased onto the single root node and de-duplicated by identity and role (with effective-date windows merged so no identity loses access), expired assignments are dropped, and all non-root nodes are deleted bottom-up. With organizations enabled: the organizations and their memberships stay untouched, the nodes beneath them are deleted bottom-up, and the assignments made at those nodes are dropped (`assignments_dropped_scoped`) rather than widened to the organization or the root. Either way the `hierarchy_schema` is cleared and the access model is set to `flat`. Returns `200` with the summary; `400` when the Environment is already flat, `404` when the slug does not exist. Emits an `application.hierarchy_reverted` audit event.","operationId":"PortalEnvironmentsController_revertToFlat","parameters":[{"name":"envSlug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Assignments consolidated to root, nodes deleted, schema cleared. Sibling envs in the same Application are unaffected.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RevertToFlatResponseDto"}}}}}},"400":{"description":"This Environment is already using flat access model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"This Environment is already using flat access model","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/revert-to-flat","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments","method":"GET"}}}}},"404":{"description":"Environment not found in this Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Environment not found in this Application","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/revert-to-flat","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Revert this Environment from hierarchy to flat","tags":["portal-environments"],"x-canopy-required-authorization":{"capability":{"key":"applications.manage","description":"Create, edit, and delete Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes":{"post":{"description":"Creates a hierarchy node in the active Application's current Environment. Prerequisite: the Environment must have a hierarchy schema — define it first via `PATCH /api/v1/hierarchy-schema` (or the Console's enable-hierarchy wizard), or this endpoint returns `400` (\"Hierarchy schema is not configured\"). 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 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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create a hierarchy node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.manage","description":"Create, edit, move, and delete the Environment's hierarchy nodes and edit its levels","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalHierarchyController_listNodes","parameters":[],"responses":{"200":{"description":"Scoped hierarchy tree returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScopedHierarchyTreeResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List hierarchy (scoped tree)","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.view","description":"View the Environment's hierarchy tree and its schema","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a hierarchy node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.view","description":"View the Environment's hierarchy tree and its schema","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update a hierarchy node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.manage","description":"Create, edit, move, and delete the Environment's hierarchy nodes and edit its levels","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete a hierarchy node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.manage","description":"Create, edit, move, and delete the Environment's hierarchy nodes and edit its levels","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get subtree from a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.view","description":"View the Environment's hierarchy tree and its schema","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get ancestor chain for a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.view","description":"View the Environment's hierarchy tree and its schema","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get direct children of a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.view","description":"View the Environment's hierarchy tree and its schema","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}/move","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Move (reparent) a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_structure.manage","description":"Create, edit, move, and delete the Environment's hierarchy nodes and edit its levels","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identities at a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalHierarchyController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/nodes/{id}/identities/summary","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get identity summary for a node","tags":["portal-hierarchy"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations":{"get":{"description":"Lists the Environment's organizations with `member_count` and `pending_invite_count` per row, paginated and searchable by name or slug (`q`), or looked up exactly by your own id (`external_id`). Empty while the Environment has organizations disabled. Requires the `hierarchy.view` permission.","operationId":"PortalOrganizationsController_listOrganizations","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":"external_id","required":false,"in":"query","description":"Exact match on the organization's `external_id` (your own id for it). Answers one organization or none.","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/OrganizationResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List organizations","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.view","description":"View the Environment's organizations and whether the organizations container is on","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Creates an organization from a `name` (slug auto-derived, display-only) with optional `description`, `metadata`, and `external_id` (your own id for it, unique per Environment; a repeat answers `409 organization.external_id_conflict`, so create-then-store is idempotent). The Environment must have organizations enabled (`409 organization.not_enabled` otherwise); the container is independent of the access model, so flat and hierarchy Environments both hold organizations. Requires the `hierarchy.manage` permission and emits `organization.created`.","operationId":"PortalOrganizationsController_createOrganization","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationDto"}}}},"responses":{"201":{"description":"Organization created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"409":{"description":"Organizations are not enabled on this Environment; switch the container on first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Organizations are not enabled on this Environment; switch the container on first","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes every live organization in the Environment the way the single delete does: memberships and every assignment beneath each organization are removed, pending invitations bound to the organization or its tree are revoked, and each organization is soft-deleted with its subtree. Member identities are untouched. This is the step before switching the container off. Responds `204 No Content`, requires the `hierarchy.manage` permission, and emits `organization.deleted_all` with the counts.","operationId":"PortalOrganizationsController_deleteAllOrganizations","parameters":[],"responses":{"204":{"description":"Every organization deleted with its memberships, pending invitations, and subtree"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Delete every organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}":{"get":{"description":"Returns a single organization by `id` with `member_count` and `pending_invite_count`. Returns `404` when the id does not name a live organization in the current Environment. Requires the `hierarchy.view` permission at the organization.","operationId":"PortalOrganizationsController_getOrganization","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Organization returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.view","description":"View the Environment's organizations and whether the organizations container is on","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Updates an organization's `name`, `description`, `metadata`, and/or `external_id` (`null` clears it; a value already used by another organization in the Environment answers `409 organization.external_id_conflict`) by `id`. The slug is frozen at create and never changes. Supports optimistic concurrency via the `If-Match` header carrying the organization's current version (`409` on a version mismatch). Requires the `hierarchy.manage` permission at the organization and emits `organization.updated`.","operationId":"PortalOrganizationsController_updateOrganization","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/UpdateOrganizationDto"}}}},"responses":{"200":{"description":"Organization updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Deletes an organization by `id`: removes every membership and every assignment beneath the organization, revokes every pending invitation bound to it or its tree, and soft-deletes the organization with its subtree. Member identities themselves are untouched and keep their memberships in other organizations. Supports optimistic concurrency via the `If-Match` header (`409` on a version mismatch). Responds `204 No Content`, requires the `hierarchy.manage` permission at the organization, and emits `organization.deleted`.","operationId":"PortalOrganizationsController_deleteOrganization","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":"Organization 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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/policy":{"get":{"description":"Returns the organization's own authentication policy (null fields inherit the Environment) alongside the effective policy its members sign in under. Requires the `hierarchy.view` permission at the organization.","operationId":"PortalOrganizationsController_getPolicy","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Policy returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationPolicyResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/policy","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an organization's authentication policy","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.view","description":"View the Environment's organizations and whether the organizations container is on","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"patch":{"description":"Sets the organization's own authentication policy. A field set to `null` inherits the Environment again. The policy can only tighten the Environment's settings: `mfa_required` can be true where the Environment leaves MFA optional, `mfa_factor_allowlist` must be a subset of the Environment's, `mfa_after_sso` can be `require` where the Environment exempts, and `require_sso` refuses password and code logins for members. Returns `400` (`organization.policy_loosens`) when a value would loosen the Environment, `409` when `If-Match` does not match the policy version. Requires the `hierarchy.manage` permission at the organization and emits `organization.policy.updated`.","operationId":"PortalOrganizationsController_updatePolicy","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/UpdateOrganizationPolicyDto"}}}},"responses":{"200":{"description":"Policy updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationPolicyResponseDto"}}}}}},"400":{"description":"The policy would loosen the Environment's authentication settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The policy would loosen the Environment's authentication settings","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/policy","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/policy","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/policy","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an organization's authentication policy","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections":{"get":{"description":"Lists the end-user SSO connections bound to this organization, oldest first, each with the membership role a just-in-time provisioned login receives. Requires the `hierarchy.view` permission at the organization. The response is an unpaginated `{ items }` array.","operationId":"PortalOrganizationsController_listSsoConnections","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Bindings returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationSsoConnectionResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List an organization's SSO connections","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.view","description":"View the Environment's organizations and whether the organizations container is on","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Binds an `end_user` SSO connection to the organization: a login through the connection lands in this organization, joining as a member with `default_role_id` when the identity is provisioned or is not yet a member, and the session starts here. The connection must already be bound to the organization's Environment (that binding routes the email domain), and the role must be an active, non-system role of the Environment (`400` otherwise: `rbac.role_inactive` or `rbac.role_system_not_assignable`). A connection binds to one organization per Environment (`409`). Requires the `hierarchy.manage` permission at the organization and emits `organization.sso_connection.bound`.","operationId":"PortalOrganizationsController_bindSsoConnection","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BindOrganizationSsoConnectionDto"}}}},"responses":{"201":{"description":"Connection bound","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationSsoConnectionResponseDto"}}}}}},"400":{"description":"The connection is not an end-user connection bound to the organization's Environment, or the role is not an active, assignable role in that Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The connection is not an end-user connection bound to the organization's Environment, or the role is not an active, assignable role in that Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections","method":"POST"}}}}},"409":{"description":"The connection is already bound to an organization in this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The connection is already bound to an organization in this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Bind an SSO connection to an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections/{connectionId}":{"delete":{"description":"Removes the binding; logins through the connection fall back to the Environment's placement. Existing memberships are untouched. Returns `404` when the connection is not bound to this organization. Requires the `hierarchy.manage` permission at the organization and emits `organization.sso_connection.unbound`.","operationId":"PortalOrganizationsController_unbindSsoConnection","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"connectionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Connection unbound"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"This SSO connection is not bound to the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"This SSO connection is not bound to the organization","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/sso-connections/{connectionId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Unbind an SSO connection from an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_organizations.manage","description":"Switch the organizations container on and off and create, rename, and delete organizations","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members":{"get":{"description":"Lists the organization's members: each row is the member's identity, their single role in this organization, effective dates, when they joined, and `node_assignments`, the grants the member also holds at nodes beneath the organization (empty in a flat Environment). Paginated; `q` searches name and email. Requires the `rbac.view_assignments` permission at the organization.","operationId":"PortalOrganizationsController_listMembers","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"]}},{"name":"role_id","required":false,"in":"query","description":"Only members holding this role","schema":{"format":"uuid","type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by the membership's effective window: active now, scheduled to start, or already expired","schema":{"type":"string","enum":["active","scheduled","expired"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationMemberResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List an organization's members","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"post":{"description":"Adds a member: assigns `role_id` to `identity_id` at the organization. The identity must already hold an active membership in this Environment (invite by email instead for someone who does not). A member holds exactly one role per organization, so an identity that is already a member returns `409` — use the change-role operation. Requires the `rbac.manage_assignments` permission at the organization and emits `organization.member.added`.","operationId":"PortalOrganizationsController_addMember","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddOrganizationMemberDto"}}}},"responses":{"201":{"description":"Member added","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationMemberResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members","method":"POST"}}}}},"409":{"description":"The identity is already a member of this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The identity is already a member of this organization","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Add a member to an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members/{identityId}":{"patch":{"description":"Replaces the member's role in this organization with `role_id`. A member holds exactly one role per organization, so this updates the existing membership in place; changing to the role already held is a no-op. Returns `404` when the identity is not a member. Requires the `rbac.manage_assignments` permission at the organization and emits `organization.member.role_changed`.","operationId":"PortalOrganizationsController_changeMemberRole","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"identityId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeOrganizationMemberRoleDto"}}}},"responses":{"200":{"description":"Member role changed","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrganizationMemberResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"The identity is not a member of this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"The identity is not a member of this organization","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members/{identityId}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Change a member's role","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"delete":{"description":"Removes the member from this organization, revoking their role here and nothing else — the identity and its memberships in other organizations are untouched. Returns `404` when the identity is not a member. Responds `204 No Content`, requires the `rbac.manage_assignments` permission at the organization, and emits `organization.member.removed`.","operationId":"PortalOrganizationsController_removeMember","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"identityId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Member 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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"The identity is not a member of this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"The identity is not a member of this organization","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/members/{identityId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove a member from an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/invites":{"get":{"description":"Lists the organization's invitations (pending, accepted, revoked, and expired — filter with `status`), paginated. Requires the `identity.view` permission.","operationId":"PortalOrganizationsController_listInvites","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"]}},{"name":"status","required":false,"in":"query","description":"Filter by invitation status","schema":{"type":"string","enum":["pending","accepted","revoked","expired"]}}],"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List an organization's invitations","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"post":{"description":"Invites an email into the organization with the role bound to the invitation; acceptance creates the Environment membership and the organization membership in one step. Works for emails with no identity in the Environment — one is created on acceptance. Requires the `identity.manage` permission and emits `invite.created`.","operationId":"PortalOrganizationsController_createInvite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationInviteDto"}}}},"responses":{"201":{"description":"Invitation created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IdentityInviteResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Organization not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/invites","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Invite a member into an organization","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/invites/{inviteId}":{"delete":{"description":"Revokes an invitation on this organization; the emailed link stops working immediately. Returns `404` when the invitation does not belong to this organization. Requires the `identity.manage` permission and emits `invite.revoked`.","operationId":"PortalOrganizationsController_revokeInvite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"inviteId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Invitation revoked"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations","method":"GET"}}}}},"404":{"description":"No such invitation on this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No such invitation on this organization","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/organizations/{id}/invites/{inviteId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke an organization invitation","tags":["portal-organizations"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentityInvitesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List invites","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"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":"PortalIdentityInvitesController_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/IdentityInviteResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create an invite","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentityInvitesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get invite summary","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentityInvitesController_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/IdentityInviteResponseDto"}}}}}}}}},"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/IdentityInviteResponseDto"}}},{"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk-create invites","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentityInvitesController_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/MessageResponseDto"}}}}}},"400":{"description":"Only pending or expired invites can be resent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Only pending or expired invites can be resent","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites/{id}/resend","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Resend an invite","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentityInvitesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identity-invites/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke an invite","tags":["portal-identity-invites"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List roles","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.view","description":"View roles and the permissions they grant","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create a custom role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.manage","description":"Create, edit, and delete roles and set their permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.view","description":"View roles and the permissions they grant","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update a role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.manage","description":"Create, edit, and delete roles and set their permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete a role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.manage","description":"Create, edit, and delete roles and set their permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles/{id}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get permissions for a role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.view","description":"View roles and the permissions they grant","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacRolesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/roles/{id}/permissions","method":"PUT"}}}}}},"security":[{"JWT":[]}],"summary":"Replace permissions on a role","tags":["portal-rbac-roles"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.manage","description":"Create, edit, and delete roles and set their permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List permission catalog","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.view","description":"View the permission catalog","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Register new permission(s)","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.manage","description":"Create, edit, and delete permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions","method":"GET"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions/evaluate/explain","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Explain a node-scoped authorization decision","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.view","description":"View the permission catalog","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a permission","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.view","description":"View the permission catalog","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update permission metadata","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.manage","description":"Create, edit, and delete permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove a permission","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.manage","description":"Create, edit, and delete permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacPermissionsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/permissions/{id}/usage","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get where a permission is used","tags":["portal-rbac-permissions"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.view","description":"View the permission catalog","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List all assignments across the Application","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get App-wide assignment summary","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Assign a role to an identity at a node","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an assignment","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove an assignment","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk remove assignments","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk change role on assignments","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalRbacAssignmentsController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/assignments/app-wide","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk-create role assignments. All-or-nothing: any failure rolls back the entire batch","tags":["portal-rbac-assignments"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalApiKeysController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create a new API key","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.manage","description":"Create, edit, rotate, and delete API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalApiKeysController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List API keys for Application","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.view","description":"View API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys/scopes":{"get":{"description":"Returns the platform's static API-key scope catalog as an unpaginated `{ items }` list — the fixed set of capabilities a `scoped` key may be granted, independent of any Environment's RBAC permission catalog. Reference data with no authorization check.","operationId":"PortalApiKeysController_listScopes","parameters":[],"responses":{"200":{"description":"Available API key scopes returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyScopeDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the permission scopes a scoped key can hold","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.view","description":"View API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys/summary":{"get":{"description":"Active/inactive API-key counts across the entire active Environment (optionally filtered by the same `q` name search the list uses), not just the current page. Requires `api_key.view`.","operationId":"PortalApiKeysController_summary","parameters":[{"name":"q","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"API key active/inactive summary returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ActiveSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Active/inactive API key counts for the environment","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.view","description":"View API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalApiKeysController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Activate or deactivate an API key","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.manage","description":"Create, edit, rotate, and delete API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalApiKeysController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete an API key","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.manage","description":"Create, edit, rotate, and delete API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalApiKeysController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/api-keys/{id}/rotate-secret","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Rotate an API key's secret","tags":["portal-api-keys"],"x-canopy-required-authorization":{"capability":{"key":"api_keys.manage","description":"Create, edit, rotate, and delete API keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens":{"post":{"description":"Mints a new SCIM bearer token for the current Environment, returning the raw `token` exactly once alongside its `id`, `token_preview`, and the SCIM `base_url` the IdP admin pastes into Okta/Entra. The raw value is bcrypt-hashed for storage (with a SHA-256 lookup hash) and is never recoverable afterwards. Accepts an optional human `name` label and returns `201`.","operationId":"PortalScimTokensController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScimTokenDto"}}}},"responses":{"201":{"description":"Token created — the raw value is shown only once. Paste it together with the base URL into the IdP's SCIM provisioning connector (Okta, Entra).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScimTokenCreatedResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Mint a SCIM bearer token for the Environment","tags":["portal-scim-tokens"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"get":{"description":"Lists the active SCIM tokens for the current Environment, newest first, with secrets masked to a `token_preview` (revoked tokens are excluded). The response is an unpaginated `{ items }` array.","operationId":"PortalScimTokensController_list","parameters":[],"responses":{"200":{"description":"Active SCIM tokens returned (masked)","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScimTokenResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List active SCIM tokens for the Environment","tags":["portal-scim-tokens"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens/activity":{"get":{"description":"Returns the most recent SCIM sync activity for the Environment — the latest `scim.*` audit rows (provision, deprovision, group changes, mapping, and failures), newest first, capped at 20. Each row projects actor, resource, and outcome, lifting a failure `detail` from metadata so admins can debug pushes without opening the global audit log. The response is an unpaginated `{ items }` array.","operationId":"PortalScimTokensController_activity","parameters":[],"responses":{"200":{"description":"The most recent SCIM lifecycle events (provision / deprovision / group changes / mapping), newest first — sourced from the audit log","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScimActivityResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Recent SCIM sync activity for the Environment","tags":["portal-scim-tokens"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens/{id}":{"delete":{"description":"Revokes a SCIM token by `id` within the current Environment by marking it inactive; IdP requests presenting it stop validating immediately. Returns `204` with no body.","operationId":"PortalScimTokensController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Token revoked — IdP provisioning requests using it stop validating immediately"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-tokens","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke a SCIM token","tags":["portal-scim-tokens"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups":{"get":{"description":"Lists the SCIM groups the IdP has provisioned into the current Environment, ordered by display name, each annotated with its current `member_count` and any existing role/node mapping (`role_id`, `application_node_id`). The response is an unpaginated `{ items }` array.","operationId":"PortalScimGroupsController_list","parameters":[],"responses":{"200":{"description":"Groups the IdP has provisioned via SCIM, each with its role mapping (if any) and member count","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScimGroupResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List IdP-pushed SCIM groups for the Environment","tags":["portal-scim-groups"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups/{id}/mapping":{"put":{"description":"Maps an IdP-pushed SCIM group to a Canopy `role_id` at a placement `application_node_id`, then reconciles role assignments for every current member — granting the role to members that lack it (tagged with the group as source). The role and node must exist in the Environment, and system roles are rejected (`400`); responds `404` when the group, role, or node is not found. Attributed to the portal admin in an `scim.group.mapped` audit row and returns the updated group with its member count.","operationId":"PortalScimGroupsController_map","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapScimGroupDto"}}}},"responses":{"200":{"description":"Maps the group to a Canopy role at a placement node and assigns it to every current member; remapping moves existing members","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScimGroupResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","method":"GET"}}}}},"404":{"description":"No SCIM group with that id in this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SCIM group with that id in this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups/{id}/mapping","method":"PUT"}}}}}},"security":[{"JWT":[]}],"summary":"Map a SCIM group to a role at a node","tags":["portal-scim-groups"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Clears a SCIM group's role/node mapping and reconciles, revoking the assignments this group owns — except where an overlapping group still justifies a member's role, in which case ownership is handed to that sibling rather than deleted. Attributed to the portal admin in an `scim.group.unmapped` audit row; responds `404` when the group is not found in the Environment and `204` with no body on success.","operationId":"PortalScimGroupsController_unmap","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Removes the mapping and revokes the role from every member it was granted to"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups","method":"GET"}}}}},"404":{"description":"No SCIM group with that id in this Environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No SCIM group with that id in this Environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/scim-groups/{id}/mapping","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Clear a SCIM group's role mapping","tags":["portal-scim-groups"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identities in Application","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Create an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identities with their role assignments","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get Application identities summary","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Bulk-create identities","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/bulk-import-events":{"post":{"description":"Records a single audit entry summarizing a completed CSV bulk-import wizard run (`mode` of `create` or `invite`, plus `total`/`succeeded`/`failed`/`skipped` counts); the per-batch create/invite entries are emitted separately by the bulk endpoints themselves. Writes no entity rows and returns `204 No Content`. Requires the `identity.manage` permission.","operationId":"PortalIdentitiesController_logBulkImportEvent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBulkImportEventDto"}}}},"responses":{"204":{"description":"Bulk-import wizard fired its completion event. Surfaces a single audit-log entry summarising the wizard run (mode, totals) so admins can ask 'did anyone run a bulk import this week?' without correlating per-batch events."},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Record a bulk-import wizard completion","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"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 for the profile fields; a change to `is_active` writes `identity.status_set` instead, the account-wide activation switch. Returns `404` when the identity has no membership in this Environment.","operationId":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]},"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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}/detail","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get identity detail with role assignments","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}/activate":{"post":{"description":"Reactivates an identity by setting `is_active = true`, the account-wide switch, allowing it to authenticate again in every Environment it belongs to. Writes an `identity.status_set` audit row (the same event the Console's account route emits), delivered to the account's subscriptions and to those of every Environment the identity is a member of. Returns a `200` message envelope. Returns `404` when the identity has no membership in this Environment.","operationId":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}/activate","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Activate an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}/deactivate":{"post":{"description":"Deactivates an identity by setting `is_active = false`, the account-wide switch, blocking future sign-in in every Environment it belongs to. Writes an `identity.status_set` audit row (the same event the Console's account route emits), delivered to the account's subscriptions and to those of every Environment the identity is a member of. Returns a `200` message envelope. Returns `404` when the identity has no membership in this Environment.","operationId":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities/{id}/deactivate","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Deactivate an identity","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.manage","description":"Create, invite, edit, and remove identities and assign their roles","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get identity's role assignments","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalIdentitiesController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/identities","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get identity's effective permissions","tags":["portal-identities"],"x-canopy-required-authorization":{"capability":{"key":"env_identities.view","description":"View identities, invitations, and role assignments in an Environment","category":"Developer Console"},"production_people":{"key":"production_identities.access","description":"Access identities in production Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."},{"code":"environment.production_identities_forbidden","http_status":403,"message":"This Environment holds real end users. Managing people there requires production access, or is done in the Admin Workspace."}]}},"/portal/v1/me/audit-views":{"get":{"description":"Returns every saved audit-log view the calling admin has created, as a `{ items }` collection. Views are user-scoped (no Account or Application in the path) since an admin reuses them across the Accounts they belong to. Each entry carries `name`, `surface` (`admin` or `identities`), and a `filters` snapshot in `AuditQueryDto` shape. Returns an empty array when the admin has saved none.","operationId":"PortalMeAuditViewsController_list","parameters":[],"responses":{"200":{"description":"Returns every saved-view chip the admin has created on the audit-log page. Each entry carries a `name` and a `filters` payload (`AuditQueryDto` snapshot). Empty array when the admin has none.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AuditViewResponseDto"}}}}}}},"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":"/portal/v1/me/audit-views","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":"/portal/v1/me/audit-views","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List saved audit-log views for the current admin","tags":["portal-me-audit-views"]},"post":{"description":"Creates or replaces a saved audit-log view for the calling admin, keyed on `(name, surface)` — re-posting the same name and surface overwrites the prior entry, while the same name may coexist across the two surfaces. `filters` is validated as a strict `AuditQueryDto` so a stored view can always be replayed by the query endpoint. Enforces a per-admin cap of 20 views summed across both surfaces; exceeding it returns `400` (`audit.view_limit_exceeded`). Returns `201` with the stored view.","operationId":"PortalMeAuditViewsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuditViewDto"}}}},"responses":{"201":{"description":"Atomic upsert by name — POSTing a view whose name already exists overwrites the prior entry. Per-user cap of 20 views; exceeding it returns 400 with `audit.view_limit_exceeded`. The `filters` payload is validated as a strict `AuditQueryDto`.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuditViewResponseDto"}}}}}},"400":{"description":"User has reached the 20-view per-admin limit. Delete an existing view before creating a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"User has reached the 20-view per-admin limit. Delete an existing view before creating a new one.","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/me/audit-views","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":"/portal/v1/me/audit-views","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":"/portal/v1/me/audit-views","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create or replace a saved audit-log view","tags":["portal-me-audit-views"]}},"/portal/v1/me/audit-views/{surface}/{name}":{"delete":{"description":"Deletes the calling admin's saved view identified by the `:surface` and `:name` path params. `:surface` must be `admin` or `identities` (otherwise `400`). Idempotent — removing a view that doesn't exist is a no-op and still returns `204`.","operationId":"PortalMeAuditViewsController_remove","parameters":[{"name":"surface","required":true,"in":"path","schema":{"type":"string"}},{"name":"name","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Idempotent — removing a name that doesn't exist returns 204 with no error."},"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":"/portal/v1/me/audit-views","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":"/portal/v1/me/audit-views","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Delete a saved audit-log view","tags":["portal-me-audit-views"]}},"/portal/v1/accounts/{accountSlug}/me/landing":{"get":{"operationId":"PortalMeLandingController_getLanding","parameters":[],"responses":{"200":{"description":"The user's Console/Workspace access modes and entry landing","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccessLandingResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/me/landing","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":"/portal/v1/accounts/{accountSlug}/me/landing","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Resolve the current user's product access and landing","tags":["portal-me"]}},"/portal/v1/accounts/{accountSlug}/me/console-capabilities":{"get":{"operationId":"PortalMeLandingController_getMyConsoleCapabilities","parameters":[],"responses":{"200":{"description":"The Console capabilities the caller effectively holds","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminCapabilityDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/me/landing","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":"/portal/v1/accounts/{accountSlug}/me/landing","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the current user's own Developer Console capabilities","tags":["portal-me"]}},"/portal/v1/accounts/{accountSlug}/me/governance-capabilities":{"get":{"operationId":"PortalMeLandingController_getMyGovernanceCapabilities","parameters":[],"responses":{"200":{"description":"The governance capabilities the caller effectively holds","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminCapabilityDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/me/landing","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":"/portal/v1/accounts/{accountSlug}/me/landing","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the current user's own Account-governance capabilities","tags":["portal-me"]}},"/portal/v1/me/preferences":{"get":{"description":"Returns the calling admin's UI preferences, currently the `ui_hints_dismissed` map of dismissed hint keys → `true`. Preferences are user-scoped (no Account or Application in the path) and back the dashboard's persistent UI state. When the user has never set a preference no row is created; an empty preferences shape is returned instead.","operationId":"PortalMePreferencesController_get","parameters":[],"responses":{"200":{"description":"Returns the admin's persistent UI preferences (dismissed welcome banners, etc.). Missing keys mean the value has never been set; clients should treat them as the default.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserPreferencesResponseDto"}}}}}},"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":"/portal/v1/me/preferences","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":"/portal/v1/me/preferences","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the admin's preferences","tags":["portal-me-preferences"]},"patch":{"description":"Applies a partial update to the calling admin's preferences. Omitted fields are left untouched; the keys sent in `ui_hints_dismissed` are shallow-merged onto the stored map (JSONB `||`), so dismissing one hint preserves previously dismissed keys. Returns the full post-patch preferences so the client can replace its cache without a follow-up read.","operationId":"PortalMePreferencesController_patch","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUserPreferencesDto"}}}},"responses":{"200":{"description":"Merges the supplied preference values into the admin's existing preferences. `ui_hints_dismissed` is shallow-merged so dismissing one hint doesn't clobber others.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserPreferencesResponseDto"}}}}}},"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":"/portal/v1/me/preferences","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":"/portal/v1/me/preferences","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Update the admin's preferences","tags":["portal-me-preferences"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap":{"post":{"description":"One-shot first-run setup for the active Environment: creates `resource.action` permissions from the supplied `resources` and creates `roles`, attaching the named permission keys to each. Guarded to run only on an unconfigured Environment — it returns `409` if any custom permission or non-system role already exists. Existing permissions and roles encountered during the run are skipped rather than failing; returns `201` with counts of permissions and roles created and skipped, and emits an `access.bootstrapped` audit event.","operationId":"PortalSetupController_bootstrapAccess","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessBootstrapDto"}}}},"responses":{"201":{"description":"Permissions, roles, and role-permission joins created in a single transaction. No admin role assignment is seeded — admin users bypass RBAC; role assignments are for end-user identities.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AccessBootstrapResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap","method":"POST"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap","method":"POST"}}}}},"409":{"description":"Environment already has custom permissions or roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"Environment already has custom permissions or roles","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Bootstrap the Environment's permission catalog and roles","tags":["portal-setup"],"x-canopy-required-authorization":{"capability":{"key":"env_roles.manage","description":"Create, edit, and delete roles and set their permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/resource-permissions":{"post":{"description":"Adds more `resource.action` permissions to the active Environment after initial setup and maps them onto existing roles via `role_permissions`. Unlike the bootstrap endpoint this runs against an already-configured Environment and has no unconfigured guard; permissions that already exist are skipped and role-permission mappings that already exist are not duplicated. Each referenced role must exist in this Environment (`404` otherwise). Returns `201` with counts of permissions created and skipped and roles updated, and emits a `resource_permissions.added` audit event.","operationId":"PortalSetupController_addResourcePermissions","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourcePermissionsDto"}}}},"responses":{"201":{"description":"Permissions created and assigned to existing roles in a single transaction","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ResourcePermissionsResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap","method":"POST"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/setup/access-bootstrap","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Add resource permissions to existing roles","tags":["portal-setup"],"x-canopy-required-authorization":{"capability":{"key":"env_permissions.manage","description":"Create, edit, and delete permissions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/setup-summaries":{"get":{"description":"Returns one setup-status row per (Application × Environment) across the active Account as an `{ items }` collection, in a single query to avoid N+1 fetching on the Applications page. Each row reports `has_permissions`, `has_roles`, `has_role_permissions`, a combined `is_complete` flag (all three true), and `identities_count` — the distinct identities holding at least one assignment on a node in that Environment. Read-only and scoped to the Account resolved from the path and guard chain.","operationId":"PortalSetupSummariesController_list","parameters":[],"responses":{"200":{"description":"One row per (Application × Environment). `is_complete` is true when the env has ≥ 1 custom permission, ≥ 1 custom role, and ≥ 1 role-permission mapping on a custom role. Drives the env-card empty-states on the Tenant > Applications page.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SetupSummaryItemDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/setup-summaries","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":"/portal/v1/accounts/{accountSlug}/setup-summaries","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List setup status for every Environment in the Account","tags":["portal-setup-summaries"],"x-canopy-required-authorization":{"capability":{"key":"applications.view","description":"View the Account's Applications and Environments","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/user-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":"PortalUserInvitesController_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"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UserInviteResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List invites","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalUserInvitesController_createInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserInviteDto"}}}},"responses":{"201":{"description":"Invite created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserInviteResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create an invite","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/user-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":"PortalUserInvitesController_getInvitesSummary","parameters":[],"responses":{"200":{"description":"Invite status counts returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserInvitesSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get invite summary","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/user-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":"PortalUserInvitesController_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/MessageResponseDto"}}}}}},"400":{"description":"Only pending or expired invites can be resent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"Only pending or expired invites can be resent","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites/{id}/resend","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Resend an invite","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/user-invites/{id}":{"patch":{"description":"Replaces the administrator role (and scope) a pending invitation will grant on acceptance, so a mistake can be corrected without revoking and resending. Requires admin_governance.manage.","operationId":"PortalUserInvitesController_updateInviteIntent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserInviteIntentDto"}}}},"responses":{"200":{"description":"Invite updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserInviteResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/user-invites/{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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Change the role a pending invite grants","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalUserInvitesController_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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-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":"/portal/v1/accounts/{accountSlug}/user-invites/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke an invite","tags":["portal-user-invites"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/users":{"get":{"description":"Returns a paginated page of the Account's dashboard members joined to their `account_members` row, exposing each member's `account_role`, membership-derived `is_active` flag, and `joined_at`. Supports `q` substring search across name and email, a `status` filter (`active`/`inactive`), and sorting by `name`, `email`, `joined`, or `created`. Account-scoped via the Account auth guard.","operationId":"PortalUsersController_listUsers","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/UserResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List users in Account","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/users/summary":{"get":{"description":"Returns dashboard-member counts for the Account — `total_users`, `active_count`, and `inactive_count` — computed from `account_members` rows. Account-scoped via the Account auth guard.","operationId":"PortalUsersController_getUsersSummary","parameters":[],"responses":{"200":{"description":"User counts returned (total, assigned, unassigned)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UsersSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get Account users summary","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/users/{id}":{"get":{"description":"Returns one dashboard member's profile plus account-membership details (`account_role`, membership-derived `is_active`, `joined_at`). Responds `404` when the user is not a member of this Account. Account-scoped via the Account auth guard.","operationId":"PortalUsersController_getUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"User profile returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"User not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/users/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a user","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.view","description":"View administrators and their access","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Removes a member from the Account, deleting the `account_members` row and immediately revoking their active sessions for this Account; returns `204 No Content`. Cannot target yourself (`400`) or an Account `owner` (`403`); responds `404` when the user is not a member. Emits an `account_member.removed` audit entry.","operationId":"PortalUsersController_removeUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"User removed (assignments revoked)"},"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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"User not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/users/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove user from Account","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/users/{id}/activate":{"post":{"description":"Reactivates a deactivated Account membership: sets the member's status back to `active`, clears the session deny-list entry so the admin can sign in again, and returns a confirmation message. Responds `404` when the user is not a member of this Account. Emits a `user.activated` audit entry.","operationId":"PortalUsersController_activateUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"User 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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"User not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/users/{id}/activate","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Activate a user","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/users/{id}/deactivate":{"post":{"description":"Deactivates an Account membership and immediately revokes the member's active dashboard sessions for this Account (deny-listing outstanding access tokens, plus a realtime force-logout) so the change takes effect at once. Cannot target yourself (`400`) or an Account `owner` (`403`); responds `404` when the user is not a member. Emits a `user.deactivated` audit entry.","operationId":"PortalUsersController_deactivateUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"User 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":"/portal/v1/accounts/{accountSlug}/users","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":"/portal/v1/accounts/{accountSlug}/users","method":"GET"}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"User not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/users/{id}/deactivate","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Deactivate a user","tags":["portal-users"],"x-canopy-required-authorization":{"capability":{"key":"administrators.manage","description":"Invite, deactivate, and remove administrators","category":"Governance"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create a webhook subscription","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.manage","description":"Create, edit, rotate, and delete Environment webhook subscriptions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List webhook subscriptions","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.view","description":"View Environment webhook subscriptions and deliveries","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/summary":{"get":{"description":"Returns active/inactive subscription counts for the Account's webhooks at the account scope, with an optional `q` substring filter on the delivery URL. Requires `webhook.view`.","operationId":"PortalWebhooksController_summary","parameters":[{"name":"q","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook active/inactive summary returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ActiveSummaryDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Active/inactive webhook counts for the environment","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.view","description":"View Environment webhook subscriptions and deliveries","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List subscribable webhook event types","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.view","description":"View Environment webhook subscriptions and deliveries","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}/deliveries","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List delivery attempts for a webhook","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.view","description":"View Environment webhook subscriptions and deliveries","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get a webhook subscription","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.view","description":"View Environment webhook subscriptions and deliveries","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Update a webhook subscription","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.manage","description":"Create, edit, rotate, and delete Environment webhook subscriptions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"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":"PortalWebhooksController_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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Delete a webhook subscription","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.manage","description":"Create, edit, rotate, and delete Environment webhook subscriptions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}/rotate-secret":{"post":{"description":"Generates a new signing secret for one account-scoped webhook subscription `id`, replacing the old one. The new secret is returned only in this response and is never retrievable again; existing deliveries signed with the prior secret will no longer validate. Returns `404` for an unknown subscription, writes a `webhook.secret_rotated` audit row, and requires `webhook.manage`.","operationId":"PortalWebhooksController_rotateSecret","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook signing secret rotated — the new secret is only shown once","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSecretRotatedResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/webhooks/{id}/rotate-secret","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Rotate a webhook subscription's signing secret","tags":["portal-webhooks"],"x-canopy-required-authorization":{"capability":{"key":"env_webhooks.manage","description":"Create, edit, rotate, and delete Environment webhook subscriptions","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain":{"get":{"description":"Returns the custom auth domain configured for this Environment, including its verification status and the exact CNAME + TXT records the customer must publish. 404 when none is configured.","operationId":"PortalAuthDomainController_get","parameters":[],"responses":{"200":{"description":"The configured custom auth domain and the DNS records to publish","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthDomainResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"GET"}}}}},"404":{"description":"No custom auth domain is configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No custom auth domain is configured","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Get the environment's custom auth domain","tags":["portal-auth-domain"],"x-canopy-required-authorization":{"capability":{"key":"sso.view","description":"View SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Adds a custom auth domain (e.g. auth.yourapp.com) to this Environment so its identity-auth API and hosted pages can be served first-party from the customer's own host. Parks it pending with a TXT-challenge token; the customer publishes the DNS records, then calls verify.","operationId":"PortalAuthDomainController_add","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAuthDomainDto"}}}},"responses":{"201":{"description":"Custom auth domain added, pending DNS verification","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthDomainResponseDto"}}}}}},"400":{"description":"The domain is invalid or reserved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The domain is invalid or reserved","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"GET"}}}}},"409":{"description":"The domain is already configured elsewhere","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"The domain is already configured elsewhere","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Add a custom auth domain","tags":["portal-auth-domain"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Revokes the domain's certificate and deletes it. The Environment falls back to serving auth from the platform host.","operationId":"PortalAuthDomainController_remove","parameters":[],"responses":{"204":{"description":"Custom auth domain 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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"GET"}}}}},"404":{"description":"No custom auth domain is configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No custom auth domain is configured","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove a custom auth domain","tags":["portal-auth-domain"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain/verify":{"post":{"description":"Checks the domain's TXT challenge and CNAME, stamps it verified, and requests a TLS certificate. 400 when the DNS records aren't visible yet.","operationId":"PortalAuthDomainController_verify","parameters":[],"responses":{"200":{"description":"Domain verified; certificate requested","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/AuthDomainResponseDto"}}}}}},"400":{"description":"The DNS records aren't visible yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The DNS records aren't visible yet","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain/verify","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain","method":"GET"}}}}},"404":{"description":"No custom auth domain is configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No custom auth domain is configured","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-domain/verify","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Verify a custom auth domain's DNS","tags":["portal-auth-domain"],"x-canopy-required-authorization":{"capability":{"key":"sso.manage","description":"Manage SSO connections, domains, and directory sync","category":"Security"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys":{"get":{"description":"Returns every live publishable key on this Environment with its type. An Environment normally holds more than one — a `web` key for a browser app and a `native` key for a phone app — because the type decides how a session created with that key receives its refresh token.","operationId":"PortalPublishableKeysController_list","parameters":[],"responses":{"200":{"description":"The environment's live publishable keys","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PublishableKeyResponseDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the environment's publishable keys","tags":["portal-publishable-keys"],"x-canopy-required-authorization":{"capability":{"key":"publishable_keys.view","description":"View publishable keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"post":{"description":"Mints a new publishable key of the given type. A `web` key delivers refresh tokens as an httpOnly cookie the browser guards; a `native` key returns them in the response body for a phone app to store in the OS keychain. The type is fixed at creation — a key cannot be converted later, because sessions already issued from it carry the delivery it implied.","operationId":"PortalPublishableKeysController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePublishableKeyDto"}}}},"responses":{"201":{"description":"Publishable key created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PublishableKeyResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Create a publishable key","tags":["portal-publishable-keys"],"x-canopy-required-authorization":{"capability":{"key":"publishable_keys.manage","description":"Create, rename, and revoke publishable keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys/{id}":{"patch":{"description":"Renames a key. The name is a label with no behavioural consequence and is the only mutable field: a key's `type` cannot change, because sessions already issued from it carry the delivery that type implied. To switch type, create a new key and revoke the old one.","operationId":"PortalPublishableKeysController_rename","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePublishableKeyDto"}}}},"responses":{"200":{"description":"Publishable key renamed","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PublishableKeyResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys","method":"GET"}}}}},"404":{"description":"No such publishable key on this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No such publishable key on this environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys/{id}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Rename a publishable key","tags":["portal-publishable-keys"],"x-canopy-required-authorization":{"capability":{"key":"publishable_keys.manage","description":"Create, rename, and revoke publishable keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]},"delete":{"description":"Retires a key. Calls presenting it afterwards resolve no Environment and fail exactly as they would for a key that never existed. Sessions already issued from it are unaffected — revoking a key stops new sign-ins, it does not sign anyone out.","operationId":"PortalPublishableKeysController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Publishable key revoked"},"400":{"description":"The environment's last web key cannot be revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The environment's last web key cannot be revoked","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys/{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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys","method":"GET"}}}}},"404":{"description":"No such publishable key on this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"No such publishable key on this environment","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/publishable-keys/{id}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke a publishable key","tags":["portal-publishable-keys"],"x-canopy-required-authorization":{"capability":{"key":"publishable_keys.manage","description":"Create, rename, and revoke publishable keys","category":"Developer Console"}},"x-canopy-errors":[{"code":"account.capability_required","http_status":403,"message":"You do not have the required authority for this action."}]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context":{"get":{"operationId":"PortalWorkspaceController_getContext","parameters":[],"responses":{"200":{"description":"Workspace responsibility context returned","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceContextResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Resolve the caller's operational authority in the Environment","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/identities":{"get":{"operationId":"PortalWorkspaceController_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"]}},{"name":"role_id","required":false,"in":"query","description":"Filter to identities holding this role. Pass the literal `no_assignment` to return only members holding no role anywhere in the Environment.","schema":{"type":"string"}},{"name":"identity_id","required":false,"in":"query","description":"Fetch one identity by id, through the same scoped query the list uses. Naming an identity outside the caller's branches returns nothing rather than reaching past the clamp.","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by whether the identity can sign in","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/WorkspaceIdentityResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identities within the caller's operational scope","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/identities/{identityId}/assignments":{"get":{"operationId":"PortalWorkspaceController_listIdentityAssignments","parameters":[{"name":"identityId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Responsibilities in scope returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceIdentityAssignmentDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"404":{"description":"Identity not found or not within the caller's scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Identity not found or not within the caller's scope","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/identities/{identityId}/assignments","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List an identity's responsibilities within the caller's scope","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignable-roles":{"get":{"operationId":"PortalWorkspaceController_listAssignableRoles","parameters":[],"responses":{"200":{"description":"Grantable roles in scope returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceAssignableRoleDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the identity roles the caller may grant","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignable-nodes":{"get":{"operationId":"PortalWorkspaceController_listAssignableNodes","parameters":[],"responses":{"200":{"description":"Assignable nodes in scope returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceAssignableNodeDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the hierarchy locations the caller may assign at","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/visible-nodes":{"get":{"operationId":"PortalWorkspaceController_listVisibleNodes","parameters":[],"responses":{"200":{"description":"Visible nodes in scope returned","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceAssignableNodeDto"}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List the hierarchy nodes the caller can see in this Environment","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations":{"get":{"operationId":"PortalWorkspaceController_listInvitations","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 invitation status","schema":{"type":"string","enum":["pending","accepted","revoked","expired"]}},{"name":"invitation_id","required":false,"in":"query","description":"Fetch one invitation by id, through the same scoped query the list uses. Naming an invitation outside the caller's branches returns nothing rather than reaching past the clamp.","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/WorkspaceInvitationResponseDto"}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List identity invitations within the caller's scope","tags":["portal-workspace"]},"post":{"operationId":"PortalWorkspaceController_createInvitation","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceInvitationDto"}}}},"responses":{"201":{"description":"Invitation created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceInvitationResponseDto"}}}}}},"400":{"description":"The invitation request is invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":400,"code":null,"message":"The invitation request is invalid","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"403":{"description":"The role is outside your grant ceiling","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"The role is outside your grant ceiling","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations","method":"POST"}}}}},"409":{"description":"A matching invitation already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":409,"code":null,"message":"A matching invitation already exists","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Invite an identity into a location with a role","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/activity":{"get":{"operationId":"PortalWorkspaceController_listActivity","parameters":[{"name":"limit","required":false,"in":"query","description":"Events per page (1-100, default 25)","schema":{"minimum":1,"maximum":100,"default":25,"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Opaque base64url cursor from the previous response's `pagination.next_cursor`. Omit for the first page.","schema":{"type":"string"}},{"name":"q","required":false,"in":"query","description":"Free-text match against the people named on an event — who acted and who it was done to. One field rather than separate actor and subject filters: an operational administrator asks what happened with a person, not which side of the event they were on.","schema":{"maxLength":200,"type":"string"}},{"name":"action","required":false,"in":"query","description":"Filter to a single action key. Only keys on the Workspace activity allowlist match; anything else returns nothing rather than reaching into the wider audit log.","schema":{"type":"string"}},{"name":"node_id","required":false,"in":"query","description":"Filter to events recorded at one hierarchy node. Still intersected with the caller's accessible subtree, so naming a node outside their scope returns nothing.","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"Lower bound (inclusive).","schema":{"format":"date-time","type":"string"}},{"name":"to","required":false,"in":"query","description":"Upper bound (inclusive).","schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"description":"Cursor-paginated list","content":{"application/json":{"schema":{"type":"object","required":["items","pagination"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceActivityResponseDto"}},"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."}}}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"List scoped operational activity within the caller's scope","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/activity/export":{"get":{"operationId":"PortalWorkspaceController_exportActivity","parameters":[{"name":"limit","required":false,"in":"query","description":"Events per page (1-100, default 25)","schema":{"minimum":1,"maximum":100,"default":25,"type":"number"}},{"name":"cursor","required":false,"in":"query","description":"Opaque base64url cursor from the previous response's `pagination.next_cursor`. Omit for the first page.","schema":{"type":"string"}},{"name":"q","required":false,"in":"query","description":"Free-text match against the people named on an event — who acted and who it was done to. One field rather than separate actor and subject filters: an operational administrator asks what happened with a person, not which side of the event they were on.","schema":{"maxLength":200,"type":"string"}},{"name":"action","required":false,"in":"query","description":"Filter to a single action key. Only keys on the Workspace activity allowlist match; anything else returns nothing rather than reaching into the wider audit log.","schema":{"type":"string"}},{"name":"node_id","required":false,"in":"query","description":"Filter to events recorded at one hierarchy node. Still intersected with the caller's accessible subtree, so naming a node outside their scope returns nothing.","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"Lower bound (inclusive).","schema":{"format":"date-time","type":"string"}},{"name":"to","required":false,"in":"query","description":"Upper bound (inclusive).","schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"description":"A CSV of every event the caller could page through, under the same filters. Capped; the response says so in a header when it truncates.","content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"Download the scoped activity feed as CSV","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/activity/retention":{"get":{"operationId":"PortalWorkspaceController_getActivityRetention","parameters":[],"responses":{"200":{"description":"The Account's retention window in days, or null when no policy applies","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceActivityRetentionResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}}},"security":[{"JWT":[]}],"summary":"How long activity history is kept for this Account","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations/{invitationId}/resend":{"post":{"operationId":"PortalWorkspaceController_resendInvitation","parameters":[{"name":"invitationId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Invitation resent","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceInvitationResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"404":{"description":"Invitation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Invitation not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations/{invitationId}/resend","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Resend a scoped invitation","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations/{invitationId}":{"delete":{"operationId":"PortalWorkspaceController_revokeInvitation","parameters":[{"name":"invitationId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Invitation revoked"},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"404":{"description":"Invitation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":404,"code":null,"message":"Invitation not found","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/invitations/{invitationId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Revoke a scoped invitation","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments":{"post":{"operationId":"PortalWorkspaceController_assignRole","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceAssignmentDto"}}}},"responses":{"201":{"description":"Role assignment created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceAssignmentResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"403":{"description":"The role is outside your grant ceiling","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"The role is outside your grant ceiling","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments","method":"POST"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments","method":"POST"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments","method":"POST"}}}}}},"security":[{"JWT":[]}],"summary":"Grant an identity a role at a hierarchy node","tags":["portal-workspace"]}},"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments/{assignmentId}":{"patch":{"operationId":"PortalWorkspaceController_updateAssignment","parameters":[{"name":"assignmentId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceAssignmentDto"}}}},"responses":{"200":{"description":"Assignment updated","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WorkspaceAssignmentResponseDto"}}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","method":"GET"}}}}},"403":{"description":"The role is outside your grant ceiling","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"},"example":{"error":{"statusCode":403,"code":null,"message":"The role is outside your grant ceiling","timestamp":"2026-04-20T12:00:00.000Z","path":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments/{assignmentId}","method":"PATCH"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments/{assignmentId}","method":"PATCH"}}}}},"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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments/{assignmentId}","method":"PATCH"}}}}}},"security":[{"JWT":[]}],"summary":"Move or re-role an existing assignment","tags":["portal-workspace"]},"delete":{"operationId":"PortalWorkspaceController_removeAssignment","parameters":[{"name":"assignmentId","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/context","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":"/portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/workspace/assignments/{assignmentId}","method":"DELETE"}}}}}},"security":[{"JWT":[]}],"summary":"Remove an identity's role assignment","tags":["portal-workspace"]}}},"info":{"title":"Canopy Portal API","description":"Portal endpoints for authenticated dashboard users. Authenticate via /v1/auth/login to obtain a JWT Bearer token.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"JWT":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"JWT access token obtained via /v1/auth/login"}},"schemas":{"MessageResponseDto":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"RegisterDto":{"type":"object","properties":{"email":{"type":"string","description":"User email address"},"password":{"type":"string","description":"Password (8-128 chars, must contain uppercase, lowercase, digit, and special character)","minLength":8,"maxLength":128},"first_name":{"type":"string","description":"User first name"},"last_name":{"type":"string","description":"User last name"},"account_name":{"type":"string","description":"Account name (the customer's billing tenant + identity directory). Slug is auto-derived."},"application_name":{"type":"string","description":"Name of the first Application inside the new Account. Slug is auto-derived. Customers can add more Applications later."}},"required":["email","password","first_name","last_name","account_name","application_name"]},"CreatedAccountDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"]},"AuthApplicationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"]},"CreateAccountResponseDto":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"number"},"account":{"$ref":"#/components/schemas/CreatedAccountDto"},"application":{"$ref":"#/components/schemas/AuthApplicationDto"},"default_environment_slug":{"type":"string"}},"required":["access_token","token_type","expires_in","account","application"]},"CreateAccountDto":{"type":"object","properties":{"account_name":{"type":"string","description":"Account name (the customer's billing tenant + identity directory). Slug is auto-derived."},"application_name":{"type":"string","description":"Name of the first Application inside the new Account. Slug is auto-derived."}},"required":["account_name","application_name"]},"PendingAccountCreationResponseDto":{"type":"object","properties":{"account_name":{"type":"string","description":"Account name captured on the register form"},"application_name":{"type":"string","description":"First Application name captured on the register form"},"requested_at":{"format":"date-time","type":"string","description":"When the register form was submitted. Shown on the review screen so the owner can judge how old an unrecognised request is."},"expires_at":{"format":"date-time","type":"string","description":"When the parked intent lapses. Shown alongside `requested_at` so the review screen can say how long is left to decide."}},"required":["account_name","application_name","requested_at","expires_at"]},"InviteInfoResponseDto":{"type":"object","properties":{"email":{"type":"string"},"intent":{"type":"string","enum":["activate","add_to_app","password_reset"],"description":"What this invite does, and which form the SPA should render. `activate` creates a net-new identity — collect first name, last name, and a new password. `add_to_app` adds an existing identity to a new App — show 'sign in to confirm' UX and collect the user's existing password (verified server-side; not rotated). `password_reset` rotates the password on an existing identity — collect only the new password."},"first_name":{"type":"string"},"last_name":{"type":"string"},"app_name":{"type":"string","description":"Display name of the App the recipient is being invited to. Used by the SPA to render welcome copy without a separate lookup."},"inviter_email":{"type":"string","description":"Email of the admin who issued the invite. Optional in the response — null when the inviter has been deleted.","nullable":true},"sso_available":{"type":"boolean","description":"Admin invites only: true when the invite's Account has an active admin SSO connection covering the invitee's email domain. The SPA then accepts the invite password-less and sends the admin to SSO sign-in."}},"required":["email","intent","first_name","last_name","app_name","inviter_email"]},"InviteInfoRequestDto":{"type":"object","properties":{"token":{"type":"string","description":"Invite token from the email link"}},"required":["token"]},"AcceptInviteDto":{"type":"object","properties":{"token":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"password":{"type":"string"}},"required":["token","first_name","last_name"]},"AuthUserDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"}},"required":["id","email","first_name","last_name"]},"AuthAccountDto":{"type":"object","properties":{"account_id":{"type":"string"},"account_name":{"type":"string"},"account_slug":{"type":"string"},"account_role":{"type":"string","description":"The user's account-level role (owner | admin | member)."},"applications":{"type":"array","items":{"$ref":"#/components/schemas/AuthApplicationDto"}}},"required":["account_id","account_name","account_slug","account_role"]},"LoginResponseDto":{"type":"object","properties":{"requires_account_selection":{"type":"boolean"},"expires_in":{"type":"number"},"user":{"$ref":"#/components/schemas/AuthUserDto"},"access_token":{"type":"string"},"token_type":{"type":"string"},"default_environment_slug":{"type":"string"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/AuthAccountDto"}},"pending_account_creation":{"type":"boolean"}},"required":["requires_account_selection","expires_in"]},"LoginDto":{"type":"object","properties":{"email":{"type":"string","description":"User email address"},"password":{"type":"string","description":"User password"},"include_permissions":{"type":"boolean","description":"Include effective permissions in the access token claims (opt-in cache)","default":false},"remember":{"type":"boolean","description":"Keep the session on this device: persist the refresh cookie across browser restarts. When false (default), a session cookie is issued and the browser drops it on close.","default":false}},"required":["email","password"]},"AuthTokenResponseDto":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"number"},"user":{"$ref":"#/components/schemas/AuthUserDto"},"default_environment_slug":{"type":"string"}},"required":["access_token","token_type","expires_in"]},"SelectAccountDto":{"type":"object","properties":{"account_id":{"type":"string","description":"Account ID to bind the new access token to"}},"required":["account_id"]},"SwitchApplicationDto":{"type":"object","properties":{"application_id":{"type":"string","description":"Application ID to switch to"}},"required":["application_id"]},"RefreshTokenDto":{"type":"object","properties":{"refresh_token":{"type":"string","description":"Refresh token for backend (BFF) callers that hold the session server-side instead of in the httpOnly cookie. Omit for browser callers — the token is read from the cookie. Requires a matching secret `X-API-Key` to have the rotated token returned in the body."}}},"RevokeResponseDto":{"type":"object","properties":{"revoked_count":{"type":"number"}},"required":["revoked_count"]},"LogoutDto":{"type":"object","properties":{"revoke_all":{"type":"boolean","description":"Revoke all sessions","default":false}}},"ForgotPasswordDto":{"type":"object","properties":{"email":{"type":"string","description":"Email address"}},"required":["email"]},"ResendVerificationByTokenDto":{"type":"object","properties":{"token":{"type":"string","description":"The verification token from the email link"}},"required":["token"]},"ResetPasswordDto":{"type":"object","properties":{"token":{"type":"string","description":"Password reset token"},"password":{"type":"string","description":"New password (8-128 chars, must contain uppercase, lowercase, digit, and special character)"}},"required":["token","password"]},"ChangePasswordDto":{"type":"object","properties":{"current_password":{"type":"string","description":"Current password"},"new_password":{"type":"string","description":"New password (8-128 chars, must contain uppercase, lowercase, digit, and special character)"}},"required":["current_password","new_password"]},"AuthProfileDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"pending_email":{"type":"string","nullable":true,"description":"New email address awaiting confirmation from its inbox, or null when no change is pending."},"email_verified":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"account_role":{"type":"string","nullable":true,"enum":["owner","admin","member"]}},"required":["id","email","first_name","last_name","email_verified","created_at"]},"RequestEmailChangeDto":{"type":"object","properties":{"new_email":{"type":"string","description":"The new email address to move the account to"}},"required":["new_email"]},"ConfirmEmailChangeDto":{"type":"object","properties":{"token":{"type":"string","description":"The confirmation token from the email link"}},"required":["token"]},"SessionDto":{"type":"object","properties":{"id":{"type":"string"},"ip_address":{"type":"string","nullable":true},"user_agent":{"type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","type":"string"}},"required":["id","created_at","expires_at"]},"IdentityAuthConfigDto":{"type":"object","properties":{"password_login_enabled":{"type":"boolean","description":"Whether password (email + password) login is enabled for this Environment."},"email_otp_login_enabled":{"type":"boolean","description":"Whether passwordless email-OTP login is enabled for this Environment."},"allow_self_signup":{"type":"boolean","description":"Whether self-signup (public registration) is enabled."},"require_captcha_login":{"type":"boolean","description":"Whether login requires a CAPTCHA token."},"require_captcha_signup":{"type":"boolean","description":"Whether self-signup requires a CAPTCHA token."},"turnstile_site_key":{"type":"string","nullable":true,"description":"Public Cloudflare Turnstile site key to render the widget with. Null when neither flow requires CAPTCHA (no widget needed)."}},"required":["password_login_enabled","email_otp_login_enabled","allow_self_signup","require_captcha_login","require_captcha_signup"]},"IdentityUserDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"}},"required":["id","email","first_name","last_name"]},"IdentityMfaChallengePromptDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Opaque random token (256-bit, base64url) the client must return verbatim to a factor-verify endpoint. Server stores only its SHA-256 hash."},"available_factors":{"type":"array","description":"Factors the caller may use to satisfy this challenge. Order is informational — the client picks based on what the identity has and what the user prefers.","items":{"type":"string","enum":["totp","webauthn","email_otp","recovery_code"]}},"expires_at":{"type":"string","format":"date-time","description":"After this timestamp the challenge is invalid and the client must restart from /login."}},"required":["challenge_token","available_factors","expires_at"]},"IdentityMfaEnrollmentTotpDto":{"type":"object","properties":{"otpauth_uri":{"type":"string","description":"otpauth:// URI for the authenticator app (render as a QR code)."},"manual_entry_key":{"type":"string","description":"Base32 secret for manual authenticator entry."}},"required":["otpauth_uri","manual_entry_key"]},"IdentityMfaEnrollmentPromptDto":{"type":"object","properties":{"enrollment_token":{"type":"string","description":"Sealed enrollment token — return it verbatim to a `/v1/identity/auth/login/mfa-enroll/*` endpoint to enroll a first factor and complete login. Carries the identity + the primary amr proved at login."},"available_factors":{"type":"array","description":"Factor types the caller may enroll to satisfy MFA.","items":{"type":"string","enum":["totp","webauthn"]}},"totp":{"$ref":"#/components/schemas/IdentityMfaEnrollmentTotpDto"}},"required":["enrollment_token","available_factors"]},"IdentityLoginResponseDto":{"type":"object","properties":{"requires_mfa_challenge":{"type":"boolean","description":"True when the env requires MFA and the identity has ≥ 1 enrolled factor. The client must POST one of `/v1/identity/auth/mfa/challenge/*` with the supplied `mfa_challenge.challenge_token` to mint a session."},"expires_in":{"type":"number"},"identity":{"$ref":"#/components/schemas/IdentityUserDto"},"access_token":{"type":"string"},"token_type":{"type":"string"},"refresh_token":{"type":"string","description":"Refresh token — present ONLY for trusted backend (BFF) callers that authenticate the request with a secret `X-API-Key` scoped to this environment. Browser callers never receive it here; for them it rides exclusively in the httpOnly `ca_identity_refresh_token` cookie."},"mfa_challenge":{"$ref":"#/components/schemas/IdentityMfaChallengePromptDto"},"mfa_enrollment_pending":{"type":"boolean","description":"True when the env requires MFA, the identity has not yet enrolled a factor, and the per-env grace timer has time on it. Session is fully issued; the client should nudge the user to enroll a factor before `grace_expires_at`."},"grace_expires_at":{"type":"string","format":"date-time","description":"Wall-clock deadline by which the identity must enroll a factor; after this, login is blocked with `mfa.enrollment_required` until an admin force-resets MFA."},"requires_mfa_enrollment":{"type":"boolean","description":"True when the env requires MFA, the identity has no factor, and grace is unavailable/expired. No session is issued — the client must enroll a first factor via `login/mfa-enroll/*` with `mfa_enrollment.enrollment_token`."},"mfa_enrollment":{"$ref":"#/components/schemas/IdentityMfaEnrollmentPromptDto"},"recovery_codes":{"description":"One-time recovery codes, returned ONLY when this response completes a FIRST-factor enrollment. Shown once — the client must surface them for the user to save.","type":"array","items":{"type":"string"}}},"required":["requires_mfa_challenge","expires_in","identity"]},"IdentityLoginDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) the developer embedded in their app. It names the Environment this login targets."},"email":{"type":"string","description":"Identity email address"},"password":{"type":"string","description":"Identity password"},"turnstile_token":{"type":"string","description":"Cloudflare Turnstile token — required only when the Environment has login CAPTCHA enabled (`login_require_captcha`)."},"organization":{"type":"string","description":"The organization (id) the session should start in, for a tenanted sign-in where the app already knows which organization the person is entering. Must be one the identity belongs to; otherwise the session starts in the identity's only or most recent organization. The organization's policy (MFA, SSO) applies to this login."}},"required":["publishable_key","email","password"]},"IdentityEmailOtpStartDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) naming the Environment this login targets."},"email":{"type":"string","description":"Identity email address"},"turnstile_token":{"type":"string","description":"Cloudflare Turnstile token. Required when the Environment has `login_require_captcha` enabled — requesting a login code is a login attempt and is gated by the same per-env CAPTCHA policy as password login."}},"required":["publishable_key","email"]},"IdentityEmailOtpVerifyDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) naming the Environment this login targets."},"email":{"type":"string","description":"Identity email address"},"code":{"type":"string","description":"The 6-digit login code from the email","pattern":"^\\d{6}$","example":"123456"}},"required":["publishable_key","email","code"]},"IdentityLoginMfaEnrollTotpVerifyDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) naming the Environment this login targets."},"enrollment_token":{"type":"string","description":"The sealed enrollment_token from the login `mfa_enrollment` continuation."},"code":{"type":"string","description":"The 6-digit code from the authenticator app.","pattern":"^\\d{6}$","example":"123456"},"label":{"type":"string","description":"User-facing nickname for the factor (default \"Authenticator app\").","maxLength":80}},"required":["publishable_key","enrollment_token","code"]},"MfaWebAuthnEnrollOptionsResponseDto":{"type":"object","properties":{"enrollment_token":{"type":"string","description":"Opaque short-lived (5 min) sealed token carrying the WebAuthn challenge nonce. Must be returned to /mfa/webauthn/enroll/verify exactly."},"options":{"type":"object","description":"`PublicKeyCredentialCreationOptions` JSON to pass to `navigator.credentials.create()`. Shape per the WebAuthn Level 2 spec; key fields: `rp`, `user`, `challenge`, `pubKeyCredParams`, `excludeCredentials`, `authenticatorSelection`."}},"required":["enrollment_token","options"]},"IdentityLoginMfaEnrollWebAuthnOptionsDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) naming the Environment this login targets."},"enrollment_token":{"type":"string","description":"The sealed enrollment_token from the login `mfa_enrollment` continuation."}},"required":["publishable_key","enrollment_token"]},"IdentityLoginMfaEnrollWebAuthnVerifyDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) naming the Environment this login targets."},"enrollment_token":{"type":"string","description":"The sealed enrollment_token returned by `login/mfa-enroll/webauthn/options`."},"response":{"type":"object","description":"The PublicKeyCredential JSON returned by `navigator.credentials.create()`."},"label":{"type":"string","description":"User-facing nickname for the factor (default \"Security key\").","maxLength":80}},"required":["publishable_key","enrollment_token","response"]},"IdentityTokenResponseDto":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"number"},"refresh_token":{"type":"string","description":"Rotated refresh token — present ONLY for trusted backend (BFF) callers that authenticate the request with a secret `X-API-Key` scoped to this environment. Browser callers never receive it here; for them it rides exclusively in the httpOnly `ca_identity_refresh_token` cookie."}},"required":["access_token","token_type","expires_in"]},"IdentityOrganizationSwitchResponseDto":{"type":"object","properties":{"requires_mfa_challenge":{"type":"boolean","description":"True when the target organization requires MFA and the session has not yet presented a factor. Complete `mfa_challenge` to receive the session in the target organization."},"expires_in":{"type":"number"},"access_token":{"type":"string"},"token_type":{"type":"string"},"refresh_token":{"type":"string","description":"Rotated refresh token — present ONLY for trusted backend (BFF) callers that authenticate the request with a secret `X-API-Key` scoped to this environment. Browser callers never receive it here; for them it rides exclusively in the httpOnly `ca_identity_refresh_token` cookie."},"mfa_challenge":{"$ref":"#/components/schemas/IdentityMfaChallengePromptDto"},"mfa_enrollment_pending":{"type":"boolean","description":"True when the target organization requires MFA, the identity has no factor, and its grace window still has time on it. The session is issued in the target organization; nudge the person to enroll before `grace_expires_at`. Once the window has passed, the switch is refused with `organization.mfa_enrollment_required` and the person enrolls from the session they already hold."},"grace_expires_at":{"format":"date-time","type":"string"}},"required":["requires_mfa_challenge","expires_in"]},"SwitchOrganizationDto":{"type":"object","properties":{"org_id":{"type":"string","description":"The organization to act in next. Must be one the session's identity belongs to."},"refresh_token":{"type":"string","description":"Refresh token for backend (BFF) callers that hold the session server-side instead of in the httpOnly cookie. Omit for browser callers — the token is read from the cookie."}},"required":["org_id"]},"IdentityOrganizationRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"IdentityOrganizationResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"$ref":"#/components/schemas/IdentityOrganizationRoleDto"}},"required":["id","name","slug","role"]},"IdentityLogoutDto":{"type":"object","properties":{"refresh_token":{"type":"string","description":"The refresh token of the session to end, for callers that hold it themselves (a backend proxy, or a native app whose publishable key delivers it in the body). Omit for browser callers, whose token is read from the cookie. With neither, every session of the identity is revoked."}}},"IdentityVerifyEmailDto":{"type":"object","properties":{"token":{"type":"string","description":"Email verification token"}},"required":["token"]},"IdentityVerifyEmailCodeDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) — names the Environment the identity registered into."},"email":{"type":"string","description":"The email address being verified"},"code":{"type":"string","description":"The 6-digit verification code from the email"}},"required":["publishable_key","email","code"]},"IdentityResendVerificationDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) the developer embedded in their app. It identifies the tenant the end user belongs to."},"email":{"type":"string","description":"Identity email address"}},"required":["publishable_key","email"]},"IdentityRegisterDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) the developer embedded in their app. It identifies the Environment the new identity is registered into."},"email":{"type":"string","description":"Identity email address"},"password":{"type":"string","description":"Password (8-64 chars). No composition rules — NIST SP 800-63B aligned. A HaveIBeenPwned breach check runs server-side."},"first_name":{"type":"string","description":"First name"},"last_name":{"type":"string","description":"Last name"},"turnstile_token":{"type":"string","description":"Cloudflare Turnstile token from the widget on the signup page. Required only when the target Environment has `settings.self_signup_require_captcha` enabled; verified server-side against Cloudflare."}},"required":["publishable_key","email","password"]},"IdentityForgotPasswordDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) the developer embedded in their app. It identifies the tenant the end user belongs to."},"email":{"type":"string","description":"Identity email address"}},"required":["publishable_key","email"]},"IdentityResetPasswordCodeDto":{"type":"object","properties":{"publishable_key":{"type":"string","description":"Non-secret publishable key (`pk_…`) — names the Environment the identity belongs to."},"email":{"type":"string","description":"The identity's email address"},"code":{"type":"string","description":"The 6-digit reset code from the email"},"password":{"type":"string","description":"New password (8-64 chars). No composition rules — NIST SP 800-63B aligned. HaveIBeenPwned breach check runs server-side."}},"required":["publishable_key","email","code","password"]},"IdentityResetPasswordDto":{"type":"object","properties":{"token":{"type":"string","description":"Password reset token"},"password":{"type":"string","description":"New password (8-64 chars). No composition rules — NIST SP 800-63B aligned. HaveIBeenPwned breach check runs server-side."}},"required":["token","password"]},"IdentityChangePasswordDto":{"type":"object","properties":{"current_password":{"type":"string","description":"Current password"},"new_password":{"type":"string","description":"New password (8-64 chars). No composition rules — NIST SP 800-63B aligned. HaveIBeenPwned breach check runs server-side."}},"required":["current_password","new_password"]},"IdentityRequestEmailChangeDto":{"type":"object","properties":{"new_email":{"type":"string","description":"The new email address to move the account to"},"current_password":{"type":"string","description":"Current password — required to re-authenticate the session for password identities. Omit for passwordless/SSO identities (no local password)."}},"required":["new_email"]},"IdentityConfirmEmailChangeDto":{"type":"object","properties":{"code":{"type":"string","description":"The 6-digit confirmation code sent to the new email"}},"required":["code"]},"IdentityProfileDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"email_verified":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","email_verified","created_at"]},"IdentitySessionDto":{"type":"object","properties":{"id":{"type":"string"},"ip_address":{"type":"string","nullable":true},"user_agent":{"type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","type":"string"}},"required":["id","created_at","expires_at"]},"IdentityAcceptInviteDto":{"type":"object","properties":{"token":{"type":"string"},"first_name":{"type":"string","description":"Required for `activate` invites. Ignored for `add_to_app` and `password_reset`."},"last_name":{"type":"string","description":"Required for `activate` invites. Ignored for `add_to_app` and `password_reset`."},"password":{"type":"string","description":"Password (8-64 chars). No composition rules — NIST SP 800-63B aligned. HaveIBeenPwned breach check runs server-side. For `activate` and `password_reset` this is the new password; for `add_to_app` it is the existing password used to verify the identity."}},"required":["token","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."},"webauthn_rp_id":{"type":"string","nullable":true,"example":"acme.com","description":"For a passkey, the relying-party id it was created under and the only domain it answers on: the Environment's `webauthn_rp_id`, or Canopy's own when the Environment names none. `null` for every other factor type."}},"required":["id","type","label","enrolled_at","last_used_at","webauthn_rp_id"]},"MfaTotpEnrollStartResponseDto":{"type":"object","properties":{"enrollment_token":{"type":"string","description":"Opaque short-lived (5 min) sealed token carrying the provisional TOTP secret. Must be returned to the verify endpoint exactly."},"otpauth_uri":{"type":"string","description":"`otpauth://totp/...` URI for the authenticator app to import. Render as a QR code on the client.","example":"otpauth://totp/MFA:alice%40acme.com?secret=JBSWY3DPEHPK3PXP&issuer=MFA"},"manual_entry_key":{"type":"string","description":"Human-readable fallback for users who can't scan the QR. Base32-encoded secret string.","example":"JBSWY3DPEHPK3PXP"}},"required":["enrollment_token","otpauth_uri","manual_entry_key"]},"MfaTotpEnrollVerifyResponseDto":{"type":"object","properties":{"factor":{"$ref":"#/components/schemas/MfaFactorResponseDto"},"recovery_codes":{"description":"Ten single-use recovery codes. **Shown exactly once** — the server only stores hashes. Display, allow copy/download, and proceed only after the user confirms they have saved them.","example":["ABCD-EFGH-IJKL-MNOP","QRST-UVWX-YZ23-4567","..."],"type":"array","items":{"type":"string"}},"recovery_codes_generation":{"type":"number","description":"Monotonically-increasing generation number for this batch. Used by the portal admin surface to show \"X of 10 remaining\" against the current batch."}},"required":["factor","recovery_codes","recovery_codes_generation"]},"MfaTotpEnrollVerifyDto":{"type":"object","properties":{"enrollment_token":{"type":"string","description":"The sealed enrollment_token returned by the `start` endpoint. Carries the provisional TOTP secret server-side."},"code":{"type":"string","description":"6-digit code from the authenticator app.","example":"123456"},"label":{"type":"string","description":"User-facing nickname for the factor (\"iPhone 15\", \"Work Laptop\"). Shown on the factor-management surface.","example":"iPhone 15","maxLength":80}},"required":["enrollment_token","code","label"]},"MfaWebAuthnEnrollVerifyResponseDto":{"type":"object","properties":{"factor":{"$ref":"#/components/schemas/MfaFactorResponseDto"},"recovery_codes":{"nullable":true,"description":"Ten freshly-minted single-use recovery codes — **only present on the first factor enrollment**. Subsequent enrollments return `null` here because the prior batch is still valid; the user already saw it the first time around.","example":["ABCD-EFGH-IJKL-MNOP","QRST-UVWX-YZ23-4567","..."],"type":"array","items":{"type":"string"}},"recovery_codes_generation":{"type":"number","nullable":true,"description":"Generation number of the active batch — `null` when no codes were issued from this enrollment (subsequent factor)."}},"required":["factor"]},"MfaWebAuthnEnrollVerifyDto":{"type":"object","properties":{"enrollment_token":{"type":"string","description":"The sealed enrollment_token returned by /mfa/webauthn/enroll/options. Carries the WebAuthn challenge nonce server-side."},"response":{"type":"object","description":"The PublicKeyCredential JSON returned by `navigator.credentials.create()` — id, rawId, type, response { clientDataJSON, attestationObject, transports }, clientExtensionResults."},"label":{"type":"string","description":"User-facing nickname for the factor (\"YubiKey 5C\", \"MacBook passkey\").","example":"MacBook passkey","maxLength":80}},"required":["enrollment_token","response","label"]},"MfaStepUpResponseDto":{"type":"object","properties":{"step_up_token":{"type":"string","description":"Opaque sealed token (AES-256-GCM, base64url). Pass back on the next mutation via the `X-Mfa-Step-Up-Token` header. Valid for 5 minutes."},"expires_at":{"type":"string","format":"date-time","description":"After this timestamp the token is rejected and the caller must POST /mfa/step-up again."}},"required":["step_up_token","expires_at"]},"MfaStepUpDto":{"type":"object","properties":{"factor":{"type":"string","enum":["totp","recovery_code"],"description":"The factor type the caller is satisfying. WebAuthn step-up lands once WebAuthn factors are enrollable."},"code":{"type":"string","description":"A 6-digit TOTP code or a single-use recovery code (case + dash insensitive)."}},"required":["factor","code"]},"MfaStepUpWebAuthnOptionsResponseDto":{"type":"object","properties":{"transit_token":{"type":"string","description":"Sealed 5-min token carrying the WebAuthn nonce. Must be returned to /verify exactly."},"options":{"type":"object","description":"`PublicKeyCredentialRequestOptions` for navigator.credentials.get(). Shape per the WebAuthn Level 2 spec."}},"required":["transit_token","options"]},"MfaStepUpWebAuthnVerifyDto":{"type":"object","properties":{"transit_token":{"type":"string","description":"Sealed transit_token from /mfa/step-up/webauthn/options. Carries the WebAuthn challenge nonce server-side."},"response":{"type":"object","description":"The PublicKeyCredential JSON returned by `navigator.credentials.get()`."}},"required":["transit_token","response"]},"MfaRecoveryCodesResponseDto":{"type":"object","properties":{"recovery_codes":{"description":"Ten freshly-minted single-use codes. **Shown exactly once** — the prior generation is invalidated in the same call and the server only stores hashes from here on.","example":["ABCD-EFGH-IJKL-MNOP","QRST-UVWX-YZ23-4567","..."],"type":"array","items":{"type":"string"}},"recovery_codes_generation":{"type":"number","description":"Monotonically-increasing generation number for this batch. Used by the portal admin surface to show \"X of 10 remaining\" against the current batch."}},"required":["recovery_codes","recovery_codes_generation"]},"MfaTrustedDeviceResponseDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environment_id":{"type":"string","format":"uuid","description":"Environment this device was trusted under. The cookie satisfies the MFA gate only for this env; logging into a different env's surface always requires a fresh challenge."},"label":{"type":"string","nullable":true,"description":"Short human-readable label derived from the User-Agent at issuance time (e.g. 'Mozilla/5.0 ...'). Truncated to 100 chars."},"ip_address":{"type":"string","nullable":true,"description":"IP the cookie was issued from."},"created_at":{"type":"string","format":"date-time"},"last_seen_at":{"type":"string","format":"date-time","description":"Bumped on every login that presents this cookie successfully."},"expires_at":{"type":"string","format":"date-time"}},"required":["id","environment_id","created_at","last_seen_at","expires_at"]},"MfaChallengeTotpDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token returned by /v1/identity/auth/login when `requires_mfa_challenge` was true."},"code":{"type":"string","description":"6-digit code from the authenticator app.","example":"123456"},"remember_device":{"type":"boolean","description":"When true and the env's `mfa_trusted_device_days` > 0, issue a 'remember this device' cookie so future logins from this browser skip the MFA challenge. Default false."}},"required":["challenge_token","code"]},"MfaChallengeRecoveryCodeDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token returned by /v1/identity/auth/login when `requires_mfa_challenge` was true."},"code":{"type":"string","description":"A single-use recovery code shown during enrollment. Dashes and case are normalised server-side — `ABCD-EFGH-IJKL-MNOP` and `abcdefghijklmnop` are accepted equivalently.","example":"ABCD-EFGH-IJKL-MNOP"},"remember_device":{"type":"boolean","description":"When true and the env's `mfa_trusted_device_days` > 0, issue a 'remember this device' cookie so future logins from this browser skip the MFA challenge. Default false."}},"required":["challenge_token","code"]},"MfaChallengeWebAuthnOptionsResponseDto":{"type":"object","properties":{"challenge":{"type":"string","description":"Base64URL challenge nonce, per WebAuthn Level 2."},"timeout":{"type":"number"},"rpId":{"type":"string"},"allowCredentials":{"type":"object"},"userVerification":{"type":"object"},"hints":{"type":"object"},"extensions":{"type":"object"}},"required":["challenge"]},"MfaChallengeWebAuthnOptionsDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token returned by /v1/identity/auth/login when `requires_mfa_challenge` was true."}},"required":["challenge_token"]},"MfaChallengeWebAuthnVerifyDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token returned by /v1/identity/auth/login when `requires_mfa_challenge` was true."},"response":{"type":"object","description":"The PublicKeyCredential JSON returned by `navigator.credentials.get()` — id, rawId, type, response { clientDataJSON, authenticatorData, signature, userHandle }, clientExtensionResults."},"remember_device":{"type":"boolean","description":"When true and the env's `mfa_trusted_device_days` > 0, issue a 'remember this device' cookie so future logins from this browser skip the MFA challenge. Default false."}},"required":["challenge_token","response"]},"MfaChallengeEmailOtpSendDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token returned by /v1/identity/auth/login when `requires_mfa_challenge` was true."}},"required":["challenge_token"]},"MfaChallengeEmailOtpVerifyDto":{"type":"object","properties":{"challenge_token":{"type":"string","description":"Challenge token from the prior /login."},"code":{"type":"string","description":"8-digit numeric code received via email.","example":"12345678"},"remember_device":{"type":"boolean","description":"When true and the env's `mfa_trusted_device_days` > 0, issue a 'remember this device' cookie so future logins from this browser skip the MFA challenge."}},"required":["challenge_token","code"]},"OAuthAuthorizeDto":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["email","password","client_id","redirect_uri","response_type","scope","state","code_challenge","code_challenge_method"]},"OAuthLoginEmailOtpStartDto":{"type":"object","properties":{"email":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["email","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthLoginEmailOtpVerifyDto":{"type":"object","properties":{"email":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"},"code":{"type":"string","description":"The 6-digit login code received via email.","pattern":"^\\d{6}$","example":"123456"},"remember_device":{"type":"boolean"}},"required":["email","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method","code"]},"OAuthRegisterDto":{"type":"object","properties":{"email":{"type":"string","description":"Identity email address"},"password":{"type":"string","description":"Password for the new identity"},"first_name":{"type":"string","description":"First name"},"last_name":{"type":"string","description":"Last name"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["email","password","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaChallengeDto":{"type":"object","properties":{"challenge_token":{"type":"string"},"factor":{"type":"string","enum":["totp","recovery_code"]},"code":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"},"remember_device":{"type":"boolean"}},"required":["challenge_token","factor","code","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaEmailOtpSendDto":{"type":"object","properties":{"challenge_token":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["challenge_token","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaEmailOtpVerifyDto":{"type":"object","properties":{"challenge_token":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"},"code":{"type":"string","description":"The one-time code received via email."},"remember_device":{"type":"boolean"}},"required":["challenge_token","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method","code"]},"OAuthMfaWebAuthnOptionsDto":{"type":"object","properties":{"challenge_token":{"type":"string"}},"required":["challenge_token"]},"OAuthMfaWebAuthnVerifyDto":{"type":"object","properties":{"challenge_token":{"type":"string"},"response":{"type":"object","description":"The browser-produced AuthenticationResponseJSON assertion."},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"},"remember_device":{"type":"boolean"}},"required":["challenge_token","response","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaWebAuthnEnrollOptionsDto":{"type":"object","properties":{"enrollment_token":{"type":"string"}},"required":["enrollment_token"]},"OAuthMfaWebAuthnEnrollVerifyDto":{"type":"object","properties":{"enrollment_token":{"type":"string"},"response":{"type":"object","description":"The browser-produced RegistrationResponseJSON attestation."},"label":{"type":"string","description":"User-supplied device label."},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["enrollment_token","response","label","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaEnrollVerifyDto":{"type":"object","properties":{"enrollment_token":{"type":"string"},"code":{"type":"string"},"label":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["enrollment_token","code","label","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthMfaEnrollCodesAckDto":{"type":"object","properties":{"ack_token":{"type":"string"},"client_id":{"type":"string"},"redirect_uri":{"type":"string"},"response_type":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"code_challenge":{"type":"string"},"code_challenge_method":{"type":"string"},"nonce":{"type":"string"}},"required":["ack_token","client_id","redirect_uri","response_type","scope","code_challenge","code_challenge_method"]},"OAuthTokenDto":{"type":"object","properties":{"grant_type":{"type":"string"},"code":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"redirect_uri":{"type":"string"},"code_verifier":{"type":"string"},"refresh_token":{"type":"string"}},"required":["grant_type","client_id","client_secret"]},"OAuthRevokeDto":{"type":"object","properties":{"token":{"type":"string"},"client_id":{"type":"string","description":"Client id, when authenticating with `client_secret_post`. Omit it and send `Authorization: Basic` instead for `client_secret_basic`."},"client_secret":{"type":"string","description":"Client secret, when authenticating with `client_secret_post`."},"token_type_hint":{"type":"string","description":"Which kind of token is being revoked. A hint only, per RFC 7009; the token is matched either way."}},"required":["token"]},"OAuthClientCreatedResponseDto":{"type":"object","properties":{"id":{"type":"string"},"client_id":{"type":"string"},"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"post_logout_redirect_uris":{"description":"Where this client may send someone after RP-initiated logout, matched exactly.","type":"array","items":{"type":"string"}},"grant_types":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"backchannel_logout_uri":{"type":"string","nullable":true,"description":"Where Canopy delivers a logout token when a session ends. Null means the client is told nothing."},"invite_redirect_url":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"client_secret":{"type":"string","description":"Client secret — shown only once"}},"required":["id","client_id","name","redirect_uris","post_logout_redirect_uris","grant_types","scopes","is_active","created_at","client_secret"]},"CreateOAuthClientDto":{"type":"object","properties":{"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"post_logout_redirect_uris":{"description":"Where this client may send someone after RP-initiated logout. Matched exactly, and separate from `redirect_uris`: receiving an authorization code and being a landing page after sign-out are different kinds of trust.","type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"backchannel_logout_uri":{"type":"string","description":"Where Canopy delivers a signed logout token, server to server, when a session this client issued tokens for ends. Omit it and the client is told nothing."},"invite_redirect_url":{"type":"string"}},"required":["name","redirect_uris"]},"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"]},"OAuthClientResponseDto":{"type":"object","properties":{"id":{"type":"string"},"client_id":{"type":"string"},"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"post_logout_redirect_uris":{"description":"Where this client may send someone after RP-initiated logout, matched exactly.","type":"array","items":{"type":"string"}},"grant_types":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"backchannel_logout_uri":{"type":"string","nullable":true,"description":"Where Canopy delivers a logout token when a session ends. Null means the client is told nothing."},"invite_redirect_url":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"}},"required":["id","client_id","name","redirect_uris","post_logout_redirect_uris","grant_types","scopes","is_active","created_at"]},"ActiveSummaryDto":{"type":"object","properties":{"active_count":{"type":"number","description":"Active items in the environment"},"inactive_count":{"type":"number","description":"Inactive items in the environment"}},"required":["active_count","inactive_count"]},"UpdateOAuthClientDto":{"type":"object","properties":{"name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"post_logout_redirect_uris":{"description":"Where this client may send someone after RP-initiated logout. Matched exactly. An empty array removes every destination, which leaves logout working with nowhere to return to.","type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"backchannel_logout_uri":{"type":"string","nullable":true,"description":"Where Canopy delivers a signed logout token when a session this client issued tokens for ends. `null` stops the notifications."},"invite_redirect_url":{"type":"string","nullable":true}}},"OAuthClientSecretRotatedResponseDto":{"type":"object","properties":{"client_id":{"type":"string"},"client_secret":{"type":"string","description":"New client secret — shown only once"}},"required":["client_id","client_secret"]},"AccountResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"settings":{"type":"object","additionalProperties":true},"plan":{"type":"string","nullable":true},"version":{"type":"number"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"},"delete_grace_days":{"type":"number","description":"Days a deleted account stays recoverable before it is permanently purged (config-driven)."}},"required":["id","name","slug","settings","plan","version","created_at","updated_at","delete_grace_days"]},"UpdateAccountDto":{"type":"object","properties":{"name":{"type":"string","description":"Display name shown in the dashboard chrome"},"settings":{"type":"object","description":"Account-level configuration (branding hints, SSO settings, etc.). Replaces the full settings object — merge on the client side if you only want to change one key.","additionalProperties":true}}},"AdminRoleResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_system":{"type":"boolean","description":"Platform-seeded role versus custom"},"system_key":{"type":"string","nullable":true,"description":"Stable identifier for a platform-seeded role; null for custom roles"},"is_active":{"type":"boolean"},"scope":{"type":"string","enum":["account","environment"],"description":"Whether the role is granted account-wide or within one Environment"},"capabilities":{"description":"Capability keys the role grants","type":"array","items":{"type":"string"}},"assignment_count":{"type":"number","description":"Live grants of this role across the whole Account, not just one Environment"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","is_system","is_active","scope","capabilities","assignment_count","created_at","updated_at"]},"AdminCapabilityDto":{"type":"object","properties":{"key":{"type":"string","description":"Capability key stored on an administrator role"},"description":{"type":"string","description":"Human-readable description of the capability"},"category":{"type":"string","description":"Display grouping for the role editor"},"product":{"type":"string","description":"Which product's UI surfaces this capability. Independent of `category` — `audit.view` is Security by subject and opens in the Developer Console.","enum":["console","workspace"]},"scope":{"type":"string","description":"Where this capability can be granted. `account` capabilities are held account-wide; `environment` capabilities are granted in one Environment, optionally narrowed to a hierarchy node. A role carries capabilities of exactly one scope.","enum":["account","environment"]},"ownerOnly":{"type":"boolean","description":"Never delegable to any role — held only by account ownership. Excluded from this list unless `include_owner_only=true`, since a role cannot be composed from one."}},"required":["key","description","category","product","scope","ownerOnly"]},"CreateAdminRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Administrator role name"},"description":{"type":"string","description":"Administrator role description"},"scope":{"type":"string","description":"How the role is granted. `account` roles apply account-wide and carry account-global capabilities (Console access, billing, who administers). `environment` roles are granted in one Environment, optionally narrowed to a hierarchy node, and carry the people work scoped to it. Immutable once created.","enum":["account","environment"]},"capabilities":{"description":"Capability keys the role grants. Every key must match the role's scope.","type":"array","items":{"type":"string"}}},"required":["name","scope","capabilities"]},"UpdateAdminRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Administrator role name"},"description":{"type":"string","description":"Administrator role description"},"capabilities":{"description":"Replacement set of operational capability keys","type":"array","items":{"type":"string"}},"is_active":{"type":"boolean","description":"Whether the role is active"}}},"SetGrantableRolesDto":{"type":"object","properties":{"environmentId":{"type":"string","description":"Environment the identity roles belong to"},"roleIds":{"description":"Identity role IDs this administrator role may grant","type":"array","items":{"type":"string"}}},"required":["environmentId","roleIds"]},"WorkspaceEnvironmentResponseDto":{"type":"object","properties":{"environment_id":{"type":"string"},"environment_slug":{"type":"string"},"environment_name":{"type":"string"},"application_slug":{"type":"string"},"application_name":{"type":"string"}},"required":["environment_id","environment_slug","environment_name","application_slug","application_name"]},"AdminAssignmentResponseDto":{"type":"object","properties":{"id":{"type":"string"},"admin_role_id":{"type":"string"},"user_id":{"type":"string"},"environment_id":{"type":"string","nullable":true},"scope_type":{"type":"string","enum":["account","environment","node"]},"application_node_id":{"type":"string","nullable":true},"effective_from":{"format":"date-time","type":"string","nullable":true},"effective_to":{"format":"date-time","type":"string","nullable":true},"revoked_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","admin_role_id","user_id","scope_type","created_at","updated_at"]},"CreateAdminAssignmentDto":{"type":"object","properties":{"userId":{"type":"string","description":"User receiving the administrator role"},"adminRoleId":{"type":"string","description":"Administrator role to grant"},"environmentId":{"type":"string","description":"Environment the authority applies within — omitted for an account-wide grant"},"scopeType":{"type":"string","description":"How far the authority reaches: the whole Account, one Environment, or one hierarchy node and its descendants. Must match the role's scope.","enum":["account","environment","node"]},"nodeId":{"type":"string","description":"Hierarchy node — required when scopeType is 'node'"},"effectiveFrom":{"type":"string","description":"When the authority becomes effective"},"effectiveTo":{"type":"string","description":"When the authority expires"}},"required":["userId","adminRoleId","scopeType"]},"ApplicationResponseDto":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"default_environment_id":{"type":"string","nullable":true,"description":"Id of the Environment new admin sessions land on when no env is specified (the 'default' / landing env). The dashboard marks it with a Default badge and blocks deleting it until it's repointed."},"settings":{"type":"object","additionalProperties":true},"version":{"type":"number"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","account_id","name","slug","settings","version","created_at","updated_at"]},"CreateApplicationDto":{"type":"object","properties":{"name":{"type":"string","description":"Display name for the new Application"},"slug":{"type":"string","description":"URL-safe slug. Must be lowercase alphanumeric with optional dashes (no leading/trailing dash). Auto-derived from `name` when omitted."}},"required":["name"]},"SsoConnectionResponseDto":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"scope":{"type":"string","enum":["admin","end_user"]},"type":{"type":"string","enum":["saml","oidc"]},"name":{"type":"string"},"status":{"type":"string","enum":["configuring","active","disabled","failed"]},"email_domains":{"type":"array","items":{"type":"string"}},"saml_entity_id":{"type":"string","nullable":true},"saml_sso_url":{"type":"string","nullable":true},"saml_slo_url":{"type":"string","nullable":true},"saml_signing_cert":{"type":"string","nullable":true},"saml_signature_algorithm":{"type":"string"},"saml_want_assertions_signed":{"type":"boolean"},"saml_name_id_format":{"type":"string"},"force_authn":{"type":"boolean"},"oidc_discovery_url":{"type":"string","nullable":true},"oidc_client_id":{"type":"string","nullable":true},"oidc_scopes":{"type":"array","items":{"type":"string"}},"has_oidc_client_secret":{"type":"boolean","description":"Whether an OIDC client secret is stored (never the value)."},"jit_provisioning_enabled":{"type":"boolean"},"attribute_mapping":{"type":"object","additionalProperties":true},"last_login_at":{"format":"date-time","type":"string","nullable":true},"last_login_failure_at":{"format":"date-time","type":"string","nullable":true},"consecutive_failures":{"type":"number"},"auto_disabled_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","account_id","scope","type","name","status","email_domains","saml_signature_algorithm","saml_want_assertions_signed","saml_name_id_format","force_authn","oidc_scopes","has_oidc_client_secret","jit_provisioning_enabled","attribute_mapping","consecutive_failures","created_at","updated_at"]},"CreateSsoConnectionDto":{"type":"object","properties":{"scope":{"type":"string","enum":["admin","end_user"]},"type":{"type":"string","enum":["saml","oidc"]},"name":{"type":"string","description":"Human label, e.g. \"Acme Corp Okta\"."},"email_domains":{"type":"array","items":{"type":"string"}},"saml_entity_id":{"type":"string"},"saml_sso_url":{"type":"string"},"saml_slo_url":{"type":"string"},"saml_signing_cert":{"type":"string","description":"IdP X.509 signing certificate (PEM)."},"saml_signature_algorithm":{"type":"string"},"saml_want_assertions_signed":{"type":"boolean"},"saml_name_id_format":{"type":"string"},"force_authn":{"type":"boolean","description":"Force the IdP to re-authenticate the user on every login (SAML ForceAuthn / OIDC max_age=0). Prevents silent reuse of a stale IdP session on shared devices."},"oidc_discovery_url":{"type":"string"},"oidc_client_id":{"type":"string"},"oidc_client_secret":{"type":"string","description":"OIDC client secret (plaintext in; encrypted at rest)."},"oidc_scopes":{"type":"array","items":{"type":"string"}},"attribute_mapping":{"type":"object","additionalProperties":true},"jit_provisioning_enabled":{"type":"boolean"}},"required":["scope","type","name"]},"UpdateSsoConnectionDto":{"type":"object","properties":{"name":{"type":"string","description":"Human label, e.g. \"Acme Corp Okta\"."},"email_domains":{"type":"array","items":{"type":"string"}},"saml_entity_id":{"type":"string"},"saml_sso_url":{"type":"string"},"saml_slo_url":{"type":"string"},"saml_signing_cert":{"type":"string","description":"IdP X.509 signing certificate (PEM)."},"saml_signature_algorithm":{"type":"string"},"saml_want_assertions_signed":{"type":"boolean"},"saml_name_id_format":{"type":"string"},"force_authn":{"type":"boolean","description":"Force the IdP to re-authenticate the user on every login (SAML ForceAuthn / OIDC max_age=0). Prevents silent reuse of a stale IdP session on shared devices."},"oidc_discovery_url":{"type":"string"},"oidc_client_id":{"type":"string"},"oidc_client_secret":{"type":"string","description":"OIDC client secret (plaintext in; encrypted at rest)."},"oidc_scopes":{"type":"array","items":{"type":"string"}},"attribute_mapping":{"type":"object","additionalProperties":true},"jit_provisioning_enabled":{"type":"boolean"}}},"ClaimNodeRuleResponseDto":{"type":"object","properties":{"claim":{"type":"string","description":"Claim key read from the assertion."},"value":{"type":"string","description":"Value the claim must equal (or contain)."},"node_id":{"type":"string","description":"Node to place the identity at on match."},"is_array_match":{"type":"boolean","description":"Match when the claim is an array containing `value`."}},"required":["claim","value","node_id"]},"OAuthClientLinkResponseDto":{"type":"object","properties":{"oauth_client_id":{"type":"string"},"sso_connection_id":{"type":"string"},"is_default":{"type":"boolean"},"default_hierarchy_node_id":{"type":"string","nullable":true},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleResponseDto"}},"default_role_id":{"type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["oauth_client_id","sso_connection_id","is_default","created_at"]},"ClaimNodeRuleDto":{"type":"object","properties":{"claim":{"type":"string","description":"Claim key read from the assertion."},"value":{"type":"string","description":"Value the claim must equal (or contain)."},"node_id":{"type":"string","description":"Node to place the identity at on match."},"is_array_match":{"type":"boolean","description":"Match when the claim is an array containing `value`."}},"required":["claim","value","node_id"]},"LinkOAuthClientDto":{"type":"object","properties":{"is_default":{"type":"boolean","description":"Send users of this client straight to the IdP (at most one default per client)."},"default_hierarchy_node_id":{"type":"string","nullable":true,"description":"Node a JIT'd identity lands at when no claim rule matches."},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleDto"}},"default_role_id":{"type":"string","nullable":true,"description":"Role assigned to a JIT'd identity at the resolved node."}}},"UpdateOAuthClientLinkDto":{"type":"object","properties":{"is_default":{"type":"boolean","description":"Send users of this client straight to the IdP (at most one default per client)."},"default_hierarchy_node_id":{"type":"string","nullable":true,"description":"Node a JIT'd identity lands at when no claim rule matches."},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleDto"}},"default_role_id":{"type":"string","nullable":true,"description":"Role assigned to a JIT'd identity at the resolved node."}}},"EnvironmentSsoLinkResponseDto":{"type":"object","properties":{"environment_id":{"type":"string"},"sso_connection_id":{"type":"string"},"is_default":{"type":"boolean"},"default_hierarchy_node_id":{"type":"string","nullable":true},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleResponseDto"}},"default_role_id":{"type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["environment_id","sso_connection_id","is_default","created_at"]},"LinkEnvironmentSsoDto":{"type":"object","properties":{"is_default":{"type":"boolean","description":"Default connection for this Environment's SSO routing (at most one default per Environment)."},"default_hierarchy_node_id":{"type":"string","nullable":true,"description":"Node a JIT'd identity lands at when no claim rule matches."},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleDto"}},"default_role_id":{"type":"string","nullable":true,"description":"Role assigned to a JIT'd identity at the resolved node."}}},"UpdateEnvironmentSsoLinkDto":{"type":"object","properties":{"is_default":{"type":"boolean","description":"Default connection for this Environment's SSO routing (at most one default per Environment)."},"default_hierarchy_node_id":{"type":"string","nullable":true,"description":"Node a JIT'd identity lands at when no claim rule matches."},"claim_node_mapping":{"nullable":true,"type":"array","items":{"$ref":"#/components/schemas/ClaimNodeRuleDto"}},"default_role_id":{"type":"string","nullable":true,"description":"Role assigned to a JIT'd identity at the resolved node."}}},"SsoBoundOrganizationSummaryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"environment_id":{"type":"string"}},"required":["id","name","slug","environment_id"]},"OrganizationSsoRoleSummaryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"SsoConnectionOrganizationResponseDto":{"type":"object","properties":{"organization_node_id":{"type":"string"},"organization_node":{"$ref":"#/components/schemas/SsoBoundOrganizationSummaryDto"},"sso_connection_id":{"type":"string"},"default_role_id":{"type":"string"},"default_role":{"$ref":"#/components/schemas/OrganizationSsoRoleSummaryDto"},"created_at":{"format":"date-time","type":"string"}},"required":["organization_node_id","organization_node","sso_connection_id","default_role_id","default_role","created_at"]},"SsoRecentLoginDto":{"type":"object","properties":{"outcome":{"type":"string","enum":["success","failure","denied"]},"email":{"type":"string","nullable":true,"description":"Email of the identity that signed in (snapshot)."},"reason":{"type":"string","nullable":true,"description":"Failure reason for a failed attempt (from audit metadata)."},"ip_address":{"type":"string","nullable":true},"occurred_at":{"format":"date-time","type":"string"}},"required":["outcome","occurred_at"]},"SsoDomainResponseDto":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","enum":["pending","verified","failed"]},"txt_record_name":{"type":"string","description":"DNS record name to create, e.g. `_canopy-verification.acme.com`."},"txt_record_value":{"type":"string","description":"Exact TXT value to publish at `txt_record_name`."},"verified_at":{"type":"string","nullable":true,"format":"date-time"},"last_checked_at":{"type":"string","nullable":true,"format":"date-time"},"failure_reason":{"type":"string","nullable":true}},"required":["domain","status","txt_record_name","txt_record_value","verified_at","last_checked_at","failure_reason"]},"ClaimSsoDomainDto":{"type":"object","properties":{"domain":{"type":"string","description":"Domain to claim, e.g. `acme.com`."}},"required":["domain"]},"AccountIdentityRowDto":{"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,"description":"Customer-supplied stable identifier (e.g. the IdP user ID once SSO ships). Null for identities created directly via the dashboard or API."},"is_active":{"type":"boolean","description":"Account-wide master kill switch. When false the identity has no access to any Environment regardless of EnvironmentMembership status."},"email_verified":{"type":"boolean","description":"True once the identity has clicked the email-verification link."},"environment_membership_count":{"type":"number","description":"Count of active EnvironmentMembership rows. Zero for orphans — identities that exist in the directory but haven't been added to any Environment yet (SSO-provisioned, invite-pending, or directory-first creation)."},"total_assignments":{"type":"number","description":"Total count of role assignments across every Environment in the Account. Drives the 'has any assignment' filter for surfacing orphans without role grants."},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","is_active","email_verified","environment_membership_count","total_assignments","created_at"]},"AccountIdentityEnvironmentMembershipDto":{"type":"object","properties":{"id":{"type":"string"},"environment_id":{"type":"string"},"environment_slug":{"type":"string"},"environment_name":{"type":"string"},"application_slug":{"type":"string"},"application_name":{"type":"string"},"status":{"type":"string","enum":["active","deactivated"]},"created_at":{"format":"date-time","type":"string"},"assignment_count":{"type":"number","description":"Count of role assignments this identity holds within this Environment. Renders next to the env row in the detail drawer's Environment Memberships tab so an admin can see which envs have actual access vs. which just have membership."}},"required":["id","environment_id","environment_slug","environment_name","application_slug","application_name","status","created_at","assignment_count"]},"AccountIdentityDetailResponseDto":{"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},"metadata":{"type":"object","nullable":true},"is_active":{"type":"boolean"},"erased_at":{"format":"date-time","type":"string","nullable":true,"description":"When set, the identity has been erased (GDPR/CCPA): personal data wiped, only a pseudonymous tombstone remains. Mutually exclusive with normal use — the UI shows an Erased state and hides lifecycle actions."},"email_verified":{"type":"boolean"},"email_verified_at":{"format":"date-time","type":"string","nullable":true},"locked_until":{"format":"date-time","type":"string","nullable":true,"description":"When non-null and in the future, the identity is locked out from login attempts. Set by the progressive lockout policy on repeated failed logins."},"password_changed_at":{"format":"date-time","type":"string","nullable":true},"environment_membership_count":{"type":"number"},"total_assignments":{"type":"number"},"created_at":{"format":"date-time","type":"string"},"environment_memberships":{"description":"Every active EnvironmentMembership for this identity, ordered by Application then Environment name. Each entry carries the env + app slug/name so the drawer can render links without an additional lookup, plus the per-env assignment count.","type":"array","items":{"$ref":"#/components/schemas/AccountIdentityEnvironmentMembershipDto"}}},"required":["id","email","first_name","last_name","is_active","email_verified","environment_membership_count","total_assignments","created_at","environment_memberships"]},"CreateAccountIdentityDto":{"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"},"environment_id":{"type":"string","description":"Optional Environment ID. When present, a same-transaction EnvironmentMembership row is created so the identity can sign into the named Environment immediately. Omit to create an orphan identity in the directory — the admin can grant access to one or more Environments later."}},"required":["email","first_name","last_name"]},"BulkCreateAccountIdentitiesDto":{"type":"object","properties":{"identities":{"type":"array","items":{"$ref":"#/components/schemas/CreateAccountIdentityDto"}}},"required":["identities"]},"AccountIdentitiesSummaryDto":{"type":"object","properties":{"total":{"type":"number","description":"Total identities in the Account"},"active_count":{"type":"number","description":"Identities with is_active = true"},"inactive_count":{"type":"number","description":"Identities with is_active = false (Account-wide kill switch flipped)"},"email_unverified_count":{"type":"number","description":"Identities whose email has not been verified yet"},"no_memberships_count":{"type":"number","description":"Identities with zero active EnvironmentMemberships — orphans in the directory"}},"required":["total","active_count","inactive_count","email_unverified_count","no_memberships_count"]},"UpdateAccountIdentityDto":{"type":"object","properties":{"first_name":{"type":"string","maxLength":100},"last_name":{"type":"string","maxLength":100},"metadata":{"type":"object","nullable":true,"description":"Customer-defined metadata blob (jsonb). Replaces the existing metadata on write; pass `null` to clear."}}},"UpdateAccountIdentityStatusDto":{"type":"object","properties":{"is_active":{"type":"boolean","description":"Target value for the Identity row's `is_active` flag. `false` deactivates Account-wide; `true` reactivates (per-Environment access still depends on EnvironmentMembership status)."}},"required":["is_active"]},"RevokeSessionsResponseDto":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable summary of the revocation."},"revoked_count":{"type":"number","description":"Number of active sessions that were revoked."}},"required":["message","revoked_count"]},"RevokeSessionsDto":{"type":"object","properties":{"reason":{"type":"string","description":"Optional human-readable reason recorded on the audit row (e.g. 'phishing-reported', 'password-compromise', 'policy-change'). Surfaces in the identity's activity feed during incident review.","maxLength":200}}},"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","organization","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"]},"EnvMembershipResponseDto":{"type":"object","properties":{"id":{"type":"string"},"identity_id":{"type":"string"},"environment_id":{"type":"string"},"status":{"type":"string","enum":["invited","active","deactivated","suspended"]},"invited_at":{"format":"date-time","type":"string","nullable":true},"activated_at":{"format":"date-time","type":"string","nullable":true},"deactivated_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["id","identity_id","environment_id","status","created_at"]},"AddEnvMembershipDto":{"type":"object","properties":{"environment_id":{"type":"string","description":"The Environment ID the identity should be attached to. Must belong to the same Account as the identity."}},"required":["environment_id"]},"BulkAttachEnvMembershipsDto":{"type":"object","properties":{"environment_id":{"type":"string","description":"The Environment ID to attach the identities to. Must belong to the same Account."},"identity_ids":{"description":"Identity IDs to attach to the Environment as active EnvironmentMembership rows. Each row is processed independently — pre-existing memberships and validation failures are reported per-item rather than failing the batch. Max 200 per request.","example":["id_01HXABC...","id_01HXDEF..."],"type":"array","items":{"type":"string"}}},"required":["environment_id","identity_ids"]},"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"]},"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"]},"AccountIdentityInviteResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"intent":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"client_id":{"type":"string","nullable":true,"description":"Application ID this invite optionally targets. NULL for orphan invites."},"expires_at":{"format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","intent","first_name","last_name","expires_at","created_at"]},"InviteAccountIdentityDto":{"type":"object","properties":{"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"environment_id":{"type":"string","description":"Optional Environment ID to invite the identity into. When supplied, acceptance creates an EnvironmentMembership for that Environment and the email link uses that Environment's invite_redirect_url when configured. When omitted, acceptance creates a directory-only Identity with no membership."}},"required":["email"]},"AccountMemberResponseDto":{"type":"object","properties":{"user_id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"avatar_url":{"type":"string","nullable":true},"account_role":{"type":"string","description":"Role inside the Account: typically `owner`, `admin`, `member`"},"joined_at":{"format":"date-time","type":"string"}},"required":["user_id","email","first_name","last_name","account_role","joined_at"]},"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"},"scope":{"type":"string","enum":["environment","account"],"description":"The subscription scope that takes this event. The public API lists both: everything an environment subscription accepts, then the account-only events, which need an account-scoped subscription from the Console or the portal API."},"category":{"type":"string"},"description":{"type":"string"}},"required":["event_type","scope","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"}}},"WebhookSecretRotatedResponseDto":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"secret":{"type":"string","description":"New HMAC secret — shown only once"}},"required":["id","url","secret"]},"UpdateApplicationDto":{"type":"object","properties":{"name":{"type":"string"}}},"SetDefaultEnvironmentDto":{"type":"object","properties":{"slug":{"type":"string","description":"Slug of the Environment in this Application that should become the new default."}},"required":["slug"]},"UserAppStateResponseDto":{"type":"object","properties":{"environment_slug":{"type":"string","nullable":true,"description":"Slug of the env the admin should land on for this App. Resolved server-side from the admin's last-active env, falling back to the App's default. Null only when the App has no envs."}},"required":["environment_slug"]},"UpdateUserAppStateDto":{"type":"object","properties":{"environment_slug":{"type":"string","description":"Slug of the env the admin is now active in. Must be a real env in this Application."}},"required":["environment_slug"]},"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","organization","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","organization","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"]},"EnvironmentResponseDto":{"type":"object","properties":{"id":{"type":"string"},"application_id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"is_production":{"type":"boolean","description":"Whether this Environment holds real end users. One-way — once true it cannot be set back."},"settings":{"type":"object","additionalProperties":true,"description":"Non-auth settings only (access model, hierarchy schema). Sign-in settings are served by the auth-settings endpoint."},"version":{"type":"number"},"created_at":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","application_id","name","slug","is_production","settings","version","created_at","updated_at"]},"OAuthClientRotationResponseDto":{"type":"object","properties":{"source_client_id":{"type":"string","description":"client_id of the OAuth client in the source env this rotation replaces"},"client_id":{"type":"string","description":"Freshly-minted client_id in the new env"},"client_secret":{"type":"string","description":"Raw client_secret — shown once, never returned again. Persist on receipt."}},"required":["source_client_id","client_id","client_secret"]},"WebhookSubscriptionRotationResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Freshly-minted webhook subscription id in the new env"},"url":{"type":"string","description":"Webhook delivery URL — the natural identifier customers use to map a rotation receipt to the listener that needs the new secret"},"secret":{"type":"string","description":"Raw signing secret — shown once, never returned again. Update the listener's HMAC verifier on receipt."}},"required":["id","url","secret"]},"EnvironmentCloneResponseDto":{"type":"object","properties":{"environment":{"$ref":"#/components/schemas/EnvironmentResponseDto"},"oauth_client_rotations":{"type":"array","items":{"$ref":"#/components/schemas/OAuthClientRotationResponseDto"}},"webhook_subscription_rotations":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscriptionRotationResponseDto"}}},"required":["environment","oauth_client_rotations","webhook_subscription_rotations"]},"CreateEnvironmentDto":{"type":"object","properties":{"name":{"type":"string","description":"Display name for the new Environment"},"slug":{"type":"string","description":"URL-safe slug. Lowercase alphanumeric with optional dashes (no leading/trailing dash). Auto-derived from `name` when omitted. Unique within the Application."},"copy_from":{"type":"string","description":"Source env slug to clone configuration from. Copies permissions, roles, role-permission joins, hierarchy nodes, OAuth client configs (without secrets), and webhook configs (without secrets). Identities and role assignments are NOT copied."}},"required":["name"]},"UpdateEnvironmentDto":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"is_production":{"type":"boolean","description":"Mark this Environment as holding real end users. One-way: `true` promotes it and is permanent; sending `false` for an Environment already marked production is rejected with 400. Demotion is the only change that would expose real people to anyone with Console access."}}},"EnvironmentAuthSettingsResponseDto":{"type":"object","properties":{"environment_slug":{"type":"string","description":"Environment slug the settings belong to"},"mfa_required":{"type":"boolean","description":"Effective MFA enforcement, defaults hydrated"},"mfa_grace_period_days":{"type":"number"},"mfa_trusted_device_days":{"type":"number"},"mfa_factor_allowlist":{"type":"array","items":{"type":"string"}},"mfa_after_sso":{"type":"string","enum":["exempt","require"],"description":"What an SSO login owes the MFA gate"},"webauthn_rp_id":{"type":"string","nullable":true,"description":"Relying-party id passkeys under this Environment are bound to, or null when they use Canopy's relying party."},"webauthn_origins":{"description":"Origins allowed to run WebAuthn ceremonies under `webauthn_rp_id`. Empty when the Environment uses Canopy's relying party.","type":"array","items":{"type":"string"}},"allow_self_signup":{"type":"boolean"},"self_signup_require_captcha":{"type":"boolean"},"login_require_captcha":{"type":"boolean"},"password_login_enabled":{"type":"boolean"},"email_otp_login_enabled":{"type":"boolean"},"self_signup_daily_cap":{"type":"number"},"access_token_ttl_seconds":{"type":"number"},"refresh_token_ttl_days":{"type":"number"},"allowed_web_origins":{"type":"array","items":{"type":"string"}},"sso_return_urls":{"type":"array","items":{"type":"string"}}},"required":["environment_slug","mfa_required","mfa_grace_period_days","mfa_trusted_device_days","mfa_factor_allowlist","mfa_after_sso","webauthn_rp_id","webauthn_origins"]},"MfaEnvSettingsDto":{"type":"object","properties":{"mfa_required":{"type":"boolean","description":"Require MFA for every end-user login under this Environment. When true, identities with ≥ 1 enrolled factor are challenged after password verify; identities with 0 factors are gated by the grace-period timer (Slice 4 behaviour)."},"mfa_grace_period_days":{"type":"number","description":"Number of days an identity has to enroll a factor after the env first required MFA on a login they made. 0 = no grace, must enroll before the next login. Range 0..30.","minimum":0,"maximum":30},"mfa_trusted_device_days":{"type":"number","description":"Number of days a 'remember this device' cookie remains valid. 0 disables the option entirely. Range 0..90.","minimum":0,"maximum":90},"mfa_factor_allowlist":{"type":"array","description":"Factor types end users may use under this Environment. Customers wanting TOTP/WebAuthn only (e.g. healthcare) drop `email_otp` from the list. `recovery_code` is always offered and is not configurable here.","items":{"type":"string","enum":["totp","webauthn","email_otp"]}},"mfa_after_sso":{"type":"string","enum":["exempt","require"],"description":"What an SSO login owes the MFA gate. `require` (the default) challenges an SSO login like any other when MFA is required; `exempt` trusts the identity provider's own second factor and skips the challenge for logins that arrived through SSO."},"webauthn_rp_id":{"type":"string","nullable":true,"example":"acme.com","description":"The relying-party id passkeys under this Environment are bound to: the registrable domain the customer's own pages run on (e.g. `acme.com`, which also covers `portal.acme.com`). Set it, together with `webauthn_origins`, when the Environment's app drives the identity WebAuthn endpoints from its own domain; leave it null to use Canopy's relying party, which is what Canopy's hosted pages always use. A passkey is bound to the relying party it was created under, so changing this invalidates every passkey enrolled under the previous one."},"webauthn_origins":{"example":["https://portal.acme.com"],"description":"Exact origins (scheme, host, optional port) allowed to run WebAuthn ceremonies under `webauthn_rp_id`. Every host must be the id or a subdomain of it. Required when `webauthn_rp_id` is set; must be empty when it is null.","type":"array","items":{"type":"string"}}}},"UpdateEnvironmentAuthSettingsDto":{"type":"object","properties":{"mfa":{"description":"MFA enforcement keys persisted under `env.settings`. Send the subset of keys you want to change; omitted keys retain their prior value.","allOf":[{"$ref":"#/components/schemas/MfaEnvSettingsDto"}]},"allow_self_signup":{"type":"boolean","description":"Per-environment opt-in for browser-direct, publishable-key self-signup (`POST /v1/identity/auth/register`). Off by default; persisted under `env.settings.allow_self_signup`."},"self_signup_require_captcha":{"type":"boolean","description":"Require a Cloudflare Turnstile CAPTCHA on self-signup for this Environment. Off by default; persisted under `env.settings.self_signup_require_captcha`. Enable only once the signup page renders the Turnstile widget, or registrations will be rejected."},"login_require_captcha":{"type":"boolean","description":"Require a Cloudflare Turnstile CAPTCHA on login for this Environment. Off by default; persisted under `env.settings.login_require_captcha`. Enable only once the login page renders the Turnstile widget, or logins will be rejected."},"password_login_enabled":{"type":"boolean","description":"Enable password (email + password) login for this Environment. On by default; persisted under `env.settings.password_login_enabled`. Set false for a passwordless-only Environment."},"email_otp_login_enabled":{"type":"boolean","description":"Enable passwordless email-OTP login for this Environment. Off by default (opt-in); persisted under `env.settings.email_otp_login_enabled`. When on, identities can sign in with an emailed 6-digit code via /login/email-otp/*."},"self_signup_daily_cap":{"type":"number","minimum":0,"maximum":100000,"description":"Maximum self-signup registrations allowed per UTC day for this Environment. 0 (the default) means no cap. When exceeded, registration returns 429. Persisted under `env.settings.self_signup_daily_cap`."},"access_token_ttl_seconds":{"type":"number","minimum":60,"maximum":3600,"description":"Lifetime in seconds of the access tokens (JWTs) this Environment issues to identities. Defaults to 900 (15 minutes). Persisted under `env.settings.access_token_ttl_seconds`; applies to tokens minted after the change."},"refresh_token_ttl_days":{"type":"number","minimum":1,"maximum":365,"description":"Lifetime in days of the refresh tokens this Environment issues to identities — effectively how long a session survives without re-authentication. Defaults to 180. Persisted under `env.settings.refresh_token_ttl_days`; applies to tokens minted after the change."},"allowed_web_origins":{"description":"Browser Origins allowed to call this Environment's public identity-auth API cross-origin (CORS), e.g. `https://app.example.com`. Required to use the Direct-API flow from a browser SPA; empty means no browser client is permitted (server-to-server with an API key is unaffected). Persisted under `env.settings.allowed_web_origins`.","type":"array","items":{"type":"string"}},"sso_return_urls":{"description":"Allowlist of return URLs for publishable-key (Direct-API) SSO. After the IdP round-trip, `/v1/identity/auth/sso/*` redirects back to one of these with a single-use `sso_token`; an unregistered return URL is rejected (exact match — no open redirect). Persisted under `env.settings.sso_return_urls`.","type":"array","items":{"type":"string"}}}},"EnvironmentExportDto":{"type":"object","properties":{"format_version":{"type":"number","example":1},"source_env":{"type":"object"},"access_model":{"type":"string","enum":["flat","hierarchy"]},"organizations_enabled":{"type":"boolean"},"hierarchy_schema":{"type":"object","additionalProperties":true,"nullable":true},"permissions":{"type":"array"},"roles":{"type":"array"},"nodes":{"type":"array"},"oauth_clients":{"type":"array"},"webhooks":{"type":"array"}},"required":["format_version","source_env","access_model","organizations_enabled","hierarchy_schema","permissions","roles","nodes","oauth_clients","webhooks"]},"EnvironmentImportDto":{"type":"object","properties":{"format_version":{"type":"number","example":1},"source_env":{"type":"object"},"access_model":{"type":"string","enum":["flat","hierarchy"]},"organizations_enabled":{"type":"boolean"},"hierarchy_schema":{"type":"object","additionalProperties":true,"nullable":true},"permissions":{"type":"array"},"roles":{"type":"array"},"nodes":{"type":"array"},"oauth_clients":{"type":"array"},"webhooks":{"type":"array"},"metadata":{"type":"object"}},"required":["format_version","source_env","access_model","organizations_enabled","hierarchy_schema","permissions","roles","nodes","oauth_clients","webhooks"]},"AccessModelResponseDto":{"type":"object","properties":{"model":{"type":"string","enum":["flat","hierarchy"]},"organizations_enabled":{"type":"boolean","description":"Whether the organizations container is on. Independent of `model`: flat and hierarchy both run inside it."}},"required":["model","organizations_enabled"]},"SetAccessModelDto":{"type":"object","properties":{"model":{"type":"string","enum":["flat","hierarchy"]}},"required":["model"]},"SetOrganizationsFeatureDto":{"type":"object","properties":{"enabled":{"type":"boolean","description":"`true` to reserve the tier under the root for organizations; `false` to switch the container off (requires an empty container and a flat Environment)."}},"required":["enabled"]},"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"]},"RevertToFlatResponseDto":{"type":"object","properties":{"assignments_moved":{"type":"number","description":"Number of unique (identity, role) pairs reseated at the root node."},"assignments_deduplicated":{"type":"number","description":"Number of duplicate assignments collapsed during the dedupe pass."},"assignments_expired_dropped":{"type":"number","description":"Number of expired assignments (those whose `effective_to` was already in the past) that were discarded during the flatten. Expired rows grant no access; preserving them as unbounded root rows would silently reinstate dead access, so they're dropped. Surfaced explicitly so the admin sees the cleanup actually happened."},"assignments_dropped_scoped":{"type":"number","description":"With organizations enabled: number of assignments made at nodes beneath an organization that were dropped. Such a grant is scoped to one branch of one tenant; reseating it at the organization would widen it to the whole tenant and reseating it at the root would cross tenants, so it is removed instead. Always `0` without organizations."},"nodes_deleted":{"type":"number","description":"Number of hierarchy nodes deleted as part of the flatten: every non-root node without organizations, every node beneath an organization with them."},"organizations_preserved":{"type":"number","description":"With organizations enabled: number of organizations that stayed in place with their memberships. Always `0` without organizations."},"schema_cleared":{"type":"boolean","description":"Always `true` on success — the schema is wiped from `Application.settings`."}},"required":["assignments_moved","assignments_deduplicated","assignments_expired_dropped","assignments_dropped_scoped","nodes_deleted","organizations_preserved","schema_cleared"]},"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"]},"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"]},"OrganizationResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"slug":{"type":"string","description":"Display label derived from the name at create; frozen afterwards. Organizations are addressed by `id`."},"metadata":{"type":"object"},"external_id":{"type":"string","nullable":true,"description":"Your own id for this organization, unique per Environment."},"member_count":{"type":"number","description":"Members (one role each) in this organization."},"pending_invite_count":{"type":"number","description":"Unexpired, unaccepted, unrevoked invitations."},"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 or deleting to detect concurrent edits."}},"required":["id","name","slug","metadata","member_count","pending_invite_count","created_at","updated_at","version"]},"CreateOrganizationDto":{"type":"object","properties":{"name":{"type":"string","description":"Organization name. The slug is auto-derived from it at create and never changes afterwards."},"description":{"type":"string","description":"Organization description"},"metadata":{"type":"object","description":"Free-form metadata stored on the organization, returned as sent."},"external_id":{"type":"string","description":"Your own id for this organization (the account id in your database), unique per Environment. Look the organization up with `GET /api/v1/organizations?external_id=...`; a second create with the same value answers 409, so create-then-store is idempotent.","maxLength":255}},"required":["name"]},"UpdateOrganizationDto":{"type":"object","properties":{"name":{"type":"string","description":"New organization name. The slug does not change."},"description":{"type":"string","description":"Organization description"},"metadata":{"type":"object","description":"Free-form metadata stored on the organization, returned as sent."},"external_id":{"type":"string","nullable":true,"description":"Your own id for this organization, unique per Environment. `null` clears it.","maxLength":255}}},"EffectiveOrganizationPolicyDto":{"type":"object","properties":{"mfa_required":{"type":"boolean"},"mfa_factor_allowlist":{"type":"array","items":{"type":"string","enum":["totp","webauthn","email_otp"]}},"mfa_after_sso":{"type":"string","enum":["exempt","require"]},"require_sso":{"type":"boolean"}},"required":["mfa_required","mfa_factor_allowlist","mfa_after_sso","require_sso"]},"OrganizationPolicyResponseDto":{"type":"object","properties":{"organization_id":{"type":"string","description":"The organization this policy belongs to."},"mfa_required":{"type":"boolean","nullable":true},"mfa_factor_allowlist":{"type":"array","nullable":true,"items":{"type":"string","enum":["totp","webauthn","email_otp"]}},"mfa_after_sso":{"type":"string","enum":["exempt","require"],"nullable":true},"require_sso":{"type":"boolean"},"environment":{"description":"The Environment's own settings, the floor this organization's policy tightens from.","allOf":[{"$ref":"#/components/schemas/EffectiveOrganizationPolicyDto"}]},"effective":{"description":"The policy the organization's members sign in under.","allOf":[{"$ref":"#/components/schemas/EffectiveOrganizationPolicyDto"}]},"version":{"type":"number","description":"Optimistic-concurrency version. 0 until the organization sets a policy of its own."}},"required":["organization_id","require_sso","environment","effective","version"]},"UpdateOrganizationPolicyDto":{"type":"object","properties":{"mfa_required":{"type":"boolean","nullable":true,"description":"Require MFA for this organization's members. `true` tightens an Environment that leaves MFA optional; `false` is only accepted where the Environment does not require MFA; `null` inherits."},"mfa_factor_allowlist":{"type":"array","nullable":true,"description":"Factors this organization's members may enroll and use. Must be a non-empty subset of the Environment's allowlist; `null` inherits.","items":{"type":"string","enum":["totp","webauthn","email_otp"]}},"mfa_after_sso":{"type":"string","enum":["exempt","require"],"nullable":true,"description":"Whether an SSO login still owes an MFA challenge. `require` tightens an Environment that exempts SSO; `exempt` is only accepted where the Environment exempts; `null` inherits."},"require_sso":{"type":"boolean","description":"Members must sign in through SSO. Password and email-code logins are refused for the organization's members and for any email whose domain routes to a connection bound to this organization."}}},"OrganizationSsoConnectionSummaryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["saml","oidc"]},"status":{"type":"string"}},"required":["id","name","type","status"]},"OrganizationSsoConnectionResponseDto":{"type":"object","properties":{"organization_node_id":{"type":"string"},"sso_connection_id":{"type":"string"},"sso_connection":{"$ref":"#/components/schemas/OrganizationSsoConnectionSummaryDto"},"default_role_id":{"type":"string","description":"The membership role a login through this connection receives when it joins the organization."},"default_role":{"$ref":"#/components/schemas/OrganizationSsoRoleSummaryDto"},"created_at":{"format":"date-time","type":"string"}},"required":["organization_node_id","sso_connection_id","sso_connection","default_role_id","default_role","created_at"]},"BindOrganizationSsoConnectionDto":{"type":"object","properties":{"sso_connection_id":{"type":"string","description":"An end-user SSO connection already bound to the organization's Environment."},"default_role_id":{"type":"string","description":"The membership role a login through this connection receives when it joins the organization. Must belong to the organization's Environment."}},"required":["sso_connection_id","default_role_id"]},"OrganizationMemberIdentityDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true}},"required":["id","email"]},"OrganizationMemberRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"OrganizationMemberNodeDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"node_type":{"type":"string"}},"required":["id","name","node_type"]},"OrganizationMemberNodeAssignmentDto":{"type":"object","properties":{"id":{"type":"string","description":"The assignment id."},"application_node":{"$ref":"#/components/schemas/OrganizationMemberNodeDto"},"role":{"$ref":"#/components/schemas/OrganizationMemberRoleDto"}},"required":["id","application_node","role"]},"OrganizationMemberResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"The membership (role assignment) id."},"identity":{"$ref":"#/components/schemas/OrganizationMemberIdentityDto"},"role":{"$ref":"#/components/schemas/OrganizationMemberRoleDto"},"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","description":"When the membership was created."},"node_assignments":{"description":"Assignments the member also holds at nodes beneath the organization. Empty in a flat Environment.","type":"array","items":{"$ref":"#/components/schemas/OrganizationMemberNodeAssignmentDto"}}},"required":["id","identity","role","created_at","node_assignments"]},"AddOrganizationMemberDto":{"type":"object","properties":{"identity_id":{"type":"string","description":"Identity to add. Must already hold an active membership in this Environment; invite by email instead for someone who does not."},"role_id":{"type":"string","description":"The single role the member holds in this organization, from the Environment's roles."}},"required":["identity_id","role_id"]},"ChangeOrganizationMemberRoleDto":{"type":"object","properties":{"role_id":{"type":"string","description":"The replacement role. A member holds exactly one role per organization, so this updates the existing membership in place."}},"required":["role_id"]},"CreateOrganizationInviteDto":{"type":"object","properties":{"email":{"type":"string","description":"Email to invite. Works for people with no identity in this Environment — one is created when they accept."},"role_id":{"type":"string","description":"The role the member will hold in this organization on acceptance."},"first_name":{"type":"string"},"last_name":{"type":"string"},"send_email":{"type":"boolean","description":"Set false to skip the email and deliver the returned accept URL yourself.","default":true}},"required":["email","role_id"]},"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"]},"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"]},"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"]},"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"]},"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"}}},"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"]},"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"]},"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"]},"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"]},"ApiKeyScopeDto":{"type":"object","properties":{"key":{"type":"string","description":"Scope key stored on the API key"},"description":{"type":"string","description":"Human-readable description of the capability"},"category":{"type":"string","description":"Display grouping for the dashboard wizard"}},"required":["key","description","category"]},"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"]},"ScimTokenCreatedResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"token":{"type":"string","description":"Plaintext SCIM token — shown only once"},"token_preview":{"type":"string"},"base_url":{"type":"string","description":"The per-Environment SCIM 2.0 base URL to paste into the IdP connector."}},"required":["id","token","token_preview","base_url"]},"CreateScimTokenDto":{"type":"object","properties":{"name":{"type":"string","description":"Human label for the token, shown in the portal token list (e.g. the IdP it's pasted into). Optional."}}},"ScimTokenResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"token_preview":{"type":"string","description":"Masked preview of the raw token (first 9 chars + `****`)."},"is_active":{"type":"boolean"},"last_used_at":{"format":"date-time","type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"}},"required":["id","token_preview","is_active","created_at"]},"ScimActivityResponseDto":{"type":"object","properties":{"id":{"type":"string"},"action":{"type":"string","description":"Audit action, e.g. `scim.user.deprovisioned`."},"actor_type":{"type":"string","enum":["scim","user"],"description":"`scim` for IdP-driven sync, `user` for admin mapping changes."},"actor_label":{"type":"string","nullable":true,"description":"Token name (IdP sync) or admin email (mapping change)."},"resource_type":{"type":"string","nullable":true},"resource_label":{"type":"string","nullable":true,"description":"The affected user email or group name."},"outcome":{"type":"string","enum":["success","failure","denied"]},"detail":{"type":"string","nullable":true,"description":"Why a sync failed (present on `failure` rows)."},"created_at":{"format":"date-time","type":"string"}},"required":["id","action","actor_type","outcome","created_at"]},"ScimGroupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"external_id":{"type":"string","nullable":true},"display_name":{"type":"string"},"role_id":{"type":"string","nullable":true,"description":"Mapped role id, or null while the group is unmapped."},"application_node_id":{"type":"string","nullable":true,"description":"Placement node id for the mapped role, or null."},"member_count":{"type":"number","description":"Current IdP-asserted member count."},"created_at":{"format":"date-time","type":"string"}},"required":["id","display_name","member_count","created_at"]},"MapScimGroupDto":{"type":"object","properties":{"role_id":{"type":"string","description":"The Canopy role to grant members of this group."},"application_node_id":{"type":"string","description":"The hierarchy node the role is assigned at."}},"required":["role_id","application_node_id"]},"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"]},"CreateBulkImportEventDto":{"type":"object","properties":{"mode":{"type":"string","enum":["create","invite"]},"total":{"type":"number","description":"Total rows the wizard processed (succeeded + failed)."},"succeeded":{"type":"number"},"failed":{"type":"number"},"skipped":{"type":"number","description":"Rows the admin explicitly skipped (deselected in the preview, or unsent after a cancel)."}},"required":["mode","total","succeeded","failed"]},"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"]},"UpdateIdentityDto":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"is_active":{"type":"boolean"},"metadata":{"type":"object","nullable":true}}},"AuditViewResponseDto":{"type":"object","properties":{"name":{"type":"string","description":"Display name. Unique per `(user, surface)` pair."},"surface":{"type":"string","enum":["admin","identities"],"description":"Audit-log surface this view belongs to. The dashboard renders each view on its surface's tab only."},"filters":{"type":"object","additionalProperties":true,"description":"Snapshot of the `AuditQueryDto` filter set. Echoed back verbatim so the dashboard can re-apply the exact filter chips."}},"required":["name","surface","filters"]},"AuditQueryDto":{"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","organization","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)."},"cursor":{"type":"string","description":"Opaque base64url cursor returned by the previous response. Omit for the first page."},"limit":{"type":"number","minimum":1,"maximum":200,"default":50,"description":"Page size. Defaults to 50; max 200."}}},"CreateAuditViewDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60,"description":"Display label for the chip. Trimmed; allowed characters: letters, numbers, spaces, hyphens, underscores. 1–60 chars."},"surface":{"type":"string","enum":["admin","identities"],"description":"Audit-log surface the view belongs to. Uniqueness is per-`(name, surface)` so a 'Last 24h' chip can live on both tabs."},"filters":{"description":"Filter set snapshot — same shape as the query endpoint.","allOf":[{"$ref":"#/components/schemas/AuditQueryDto"}]}},"required":["name","surface","filters"]},"AccessModesDto":{"type":"object","properties":{"console":{"type":"string","enum":["granted","none"],"description":"granted = holds `console.access`; none = does not. Console admission is account-wide and has no degrees."},"workspace":{"type":"string","enum":["scoped","account","none"],"description":"scoped = live administrator assignment (operational area); account = governance authority only (account area); none = no Workspace access. Holding both resolves to scoped — operational authority wins."}},"required":["console","workspace"]},"AccessLandingResponseDto":{"type":"object","properties":{"product_landing":{"type":"string","enum":["console","workspace","chooser","none"]},"access_modes":{"$ref":"#/components/schemas/AccessModesDto"}},"required":["product_landing","access_modes"]},"UserPreferencesResponseDto":{"type":"object","properties":{"ui_hints_dismissed":{"type":"object","description":"Map of UI hint keys the user has dismissed. Keys are stable identifiers (e.g. 'applications_intro'); a hint is dismissed when its key maps to true.","additionalProperties":{"type":"boolean"},"example":{"applications_intro":true,"access_control_intro":true}}},"required":["ui_hints_dismissed"]},"PatchUserPreferencesDto":{"type":"object","properties":{"ui_hints_dismissed":{"type":"object","description":"Partial map of UI hint keys → true. Existing dismissed keys are preserved; only the keys provided here are added.","additionalProperties":{"type":"boolean"},"example":{"applications_intro":true}}}},"AccessBootstrapResponseDto":{"type":"object","properties":{"permissions_created":{"type":"number","description":"Number of permissions created"},"roles_created":{"type":"number","description":"Number of roles created"},"skipped_permissions":{"type":"number","description":"Number of permissions skipped (already exist)"},"skipped_roles":{"type":"number","description":"Number of roles skipped (already exist)"}},"required":["permissions_created","roles_created","skipped_permissions","skipped_roles"]},"BootstrapResourceDto":{"type":"object","properties":{"name":{"type":"string","description":"Resource name (e.g. 'notes', 'projects')","example":"notes"},"actions":{"description":"Actions for this resource (e.g. 'create', 'read')","example":["create","read","update","delete"],"type":"array","items":{"type":"string"}}},"required":["name","actions"]},"BootstrapRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Role name","example":"Admin"},"description":{"type":"string","description":"Role description","example":"Full access to all resources"},"permission_keys":{"description":"Permission keys to assign to this role","example":["notes.create","notes.read","notes.update","notes.delete"],"type":"array","items":{"type":"string"}}},"required":["name","description","permission_keys"]},"AccessBootstrapDto":{"type":"object","properties":{"resources":{"description":"Resources to generate permissions for","type":"array","items":{"$ref":"#/components/schemas/BootstrapResourceDto"}},"roles":{"description":"Roles to create with their permission assignments","type":"array","items":{"$ref":"#/components/schemas/BootstrapRoleDto"}}},"required":["resources","roles"]},"ResourcePermissionsResponseDto":{"type":"object","properties":{"permissions_created":{"type":"number","description":"Number of permissions created"},"skipped_permissions":{"type":"number","description":"Number of permissions skipped (already exist)"},"roles_updated":{"type":"number","description":"Number of roles that received new permissions"}},"required":["permissions_created","skipped_permissions","roles_updated"]},"ResourcePermissionRoleDto":{"type":"object","properties":{"role_id":{"type":"string","description":"ID of an existing role"},"permission_keys":{"description":"Permission keys to assign to this role","example":["invoices.create","invoices.read"],"type":"array","items":{"type":"string"}}},"required":["role_id","permission_keys"]},"ResourcePermissionsDto":{"type":"object","properties":{"resources":{"description":"Resources to generate permissions for","type":"array","items":{"$ref":"#/components/schemas/BootstrapResourceDto"}},"role_permissions":{"description":"Permission assignments for existing roles. Empty array creates the permissions without binding them to any role.","type":"array","items":{"$ref":"#/components/schemas/ResourcePermissionRoleDto"}}},"required":["resources","role_permissions"]},"SetupSummaryItemDto":{"type":"object","properties":{"application_id":{"type":"string"},"application_slug":{"type":"string"},"environment_id":{"type":"string"},"environment_slug":{"type":"string"},"has_permissions":{"type":"boolean","description":"≥ 1 custom permission in the env"},"has_roles":{"type":"boolean","description":"≥ 1 custom (non-system) role in the env"},"has_role_permissions":{"type":"boolean","description":"≥ 1 role-permission mapping on a custom role in the env"},"is_complete":{"type":"boolean","description":"True when all three has_* flags are true"},"nodes_count":{"type":"number","description":"Hierarchy nodes in the env"},"roles_count":{"type":"number","description":"Custom (non-system) roles in the env"},"permissions_count":{"type":"number","description":"Custom (source = 'custom') permissions in the env"},"identities_count":{"type":"number","description":"Distinct identities with ≥ 1 role assignment on a node in this env"}},"required":["application_id","application_slug","environment_id","environment_slug","has_permissions","has_roles","has_role_permissions","is_complete","nodes_count","roles_count","permissions_count","identities_count"]},"UserInviteResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"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"},"is_administrator_invite":{"type":"boolean"},"admin_role_id":{"type":"string","nullable":true},"environment_id":{"type":"string","nullable":true},"scope_type":{"type":"string","nullable":true,"enum":["account","environment","node"]},"application_node_id":{"type":"string","nullable":true}},"required":["id","email","first_name","last_name","name","status","expires_at","invited_by","created_at","is_administrator_invite"]},"UserInvitesSummaryDto":{"type":"object","properties":{"total_invites":{"type":"number"},"pending_count":{"type":"number"},"accepted_count":{"type":"number"},"expired_count":{"type":"number"},"revoked_count":{"type":"number"}},"required":["total_invites","pending_count","accepted_count","expired_count","revoked_count"]},"CreateUserInviteDto":{"type":"object","properties":{"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"admin_role_id":{"type":"string","description":"Administrator role the invitee holds on acceptance. The membership and the grant are created together."},"scope_type":{"type":"string","description":"How far the granted authority reaches. Must match the role's scope.","enum":["account","environment","node"]},"environment_id":{"type":"string","description":"Environment the authority applies within — required unless scope_type is 'account'"},"application_node_id":{"type":"string","description":"Hierarchy node — required when scope_type is 'node'"}},"required":["email","first_name","last_name","admin_role_id","scope_type"]},"UpdateUserInviteIntentDto":{"type":"object","properties":{"admin_role_id":{"type":"string","description":"Administrator role the invitee holds on acceptance"},"scope_type":{"type":"string","description":"How far the granted authority reaches. Must match the role's scope.","enum":["account","environment","node"]},"environment_id":{"type":"string","description":"Environment the authority applies within — required unless scope_type is 'account'"},"application_node_id":{"type":"string","description":"Hierarchy node — required when scope_type is 'node'"}},"required":["admin_role_id","scope_type"]},"UserResponseDto":{"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"},"account_role":{"type":"string","enum":["owner","admin","member"],"description":"The member's role in this account. Owners cannot be deactivated or removed — the dashboard hides those actions for them."},"email_verified":{"type":"boolean"},"created_at":{"format":"date-time","type":"string"},"joined_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","name","is_active","account_role","email_verified","created_at","joined_at"]},"UsersSummaryDto":{"type":"object","properties":{"total_users":{"type":"number"},"active_count":{"type":"number"},"inactive_count":{"type":"number"}},"required":["total_users","active_count","inactive_count"]},"AuthDomainDnsRecordDto":{"type":"object","properties":{"type":{"type":"string","enum":["CNAME","TXT"]},"name":{"type":"string","description":"The record name/host to create, e.g. auth.yourapp.com."},"value":{"type":"string","description":"The record value to set."}},"required":["type","name","value"]},"AuthDomainResponseDto":{"type":"object","properties":{"id":{"type":"string"},"domain":{"type":"string","example":"auth.yourapp.com"},"tls_status":{"type":"string","enum":["pending","issued","failed"],"description":"TLS certificate status for the domain."},"dns_verified_at":{"format":"date-time","type":"string","nullable":true,"description":"When the DNS challenge last verified, or null while still pending."},"active":{"type":"boolean","description":"True once the domain is DNS-verified AND its certificate is issued — the only state in which it serves live traffic."},"dns_records":{"description":"The DNS records to publish to activate this domain.","type":"array","items":{"$ref":"#/components/schemas/AuthDomainDnsRecordDto"}}},"required":["id","domain","tls_status","dns_verified_at","active","dns_records"]},"AddAuthDomainDto":{"type":"object","properties":{"domain":{"type":"string","description":"The hostname to serve this Environment's auth from, e.g. auth.yourapp.com. Must be a subdomain you control.","example":"auth.yourapp.com"}},"required":["domain"]},"PublishableKeyResponseDto":{"type":"object","properties":{"id":{"type":"string"},"key":{"type":"string","description":"The `pk_…` value to embed in the app. Non-secret by design: it names the Environment but grants nothing on its own."},"type":{"type":"string","enum":["web","native"],"description":"What kind of app holds this key, which decides refresh-token delivery for sessions created with it."},"name":{"type":"string"},"created_at":{"format":"date-time","type":"string"}},"required":["id","key","type","name","created_at"]},"CreatePublishableKeyDto":{"type":"object","properties":{"type":{"type":"string","enum":["web","native"],"description":"What kind of app will hold this key, which decides how a session created with it receives its refresh token. `web` delivers an httpOnly cookie the browser guards; `native` returns the token in the response body for a phone app to store in the OS keychain. Fixed at creation — a key cannot be converted later, because sessions already issued from it carry the delivery it implied."},"name":{"type":"string","description":"Label shown in the dashboard, so several keys on one environment are tellable apart (e.g. \"iOS app\", \"marketing site\").","minLength":1,"maxLength":100}},"required":["type","name"]},"UpdatePublishableKeyDto":{"type":"object","properties":{"name":{"type":"string","description":"Label shown in the dashboard, so several keys on one environment are tellable apart.","minLength":1,"maxLength":100}},"required":["name"]},"WorkspaceContextResponseDto":{"type":"object","properties":{"has_access":{"type":"boolean","description":"Whether the user has any operational authority here"},"environment_wide":{"type":"boolean","description":"True when authority spans the whole Environment"},"accessible_node_ids":{"description":"Hierarchy nodes the user may see/act within (empty when environment-wide or no access)","type":"array","items":{"type":"string"}},"capabilities":{"description":"Operational capabilities the user's roles grant in scope","type":"array","items":{"type":"string"}}},"required":["has_access","environment_wide","accessible_node_ids","capabilities"]},"WorkspaceIdentityRoleDto":{"type":"object","properties":{"assignment_id":{"type":"string"},"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"]},"WorkspaceIdentityResponseDto":{"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/WorkspaceIdentityRoleDto"}},"created_at":{"format":"date-time","type":"string"}},"required":["id","email","first_name","last_name","name","is_active","roles","created_at"]},"WorkspaceIdentityAssignmentRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"WorkspaceIdentityAssignmentDto":{"type":"object","properties":{"id":{"type":"string"},"role":{"$ref":"#/components/schemas/WorkspaceIdentityAssignmentRoleDto"},"application_node_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"}},"required":["id","role","application_node_id","created_at"]},"WorkspaceAssignableRoleDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true}},"required":["id","name"]},"WorkspaceAssignableNodeDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"node_type":{"type":"string"},"parent_node_id":{"type":"string","nullable":true}},"required":["id","name","node_type"]},"WorkspaceInvitationResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"name":{"type":"string"},"role_id":{"type":"string","nullable":true},"role_name":{"type":"string","nullable":true},"node_id":{"type":"string","nullable":true},"node_name":{"type":"string","nullable":true},"has_initial_assignment":{"type":"boolean","description":"True when the invitation carries a starting role at a location, so accepting it grants access rather than only creating the identity."},"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","first_name","last_name","name","has_initial_assignment","status","expires_at","invited_by","created_at"]},"WorkspaceActivityResponseDto":{"type":"object","properties":{"id":{"type":"string"},"action":{"type":"string"},"actor_label":{"type":"string","nullable":true},"resource_type":{"type":"string","nullable":true},"resource_label":{"type":"string","nullable":true},"application_node_id":{"type":"string","nullable":true},"application_node_path":{"type":"string","nullable":true},"created_at":{"format":"date-time","type":"string"},"subject_type":{"type":"string","enum":["identity","invitation"],"nullable":true,"description":"What the event is about, when the Workspace has a surface that opens it. Null for events with nothing to open."},"subject_id":{"type":"string","nullable":true,"description":"Id of the subject named by `subject_type`. For a role assignment this is the identity, not the assignment row — the assignment id names nothing a reader recognises."}},"required":["id","action","created_at"]},"WorkspaceActivityRetentionResponseDto":{"type":"object","properties":{"retention_days":{"type":"number","nullable":true,"description":"Days of activity history the Account's plan keeps, or null when no policy applies"}},"required":["retention_days"]},"CreateWorkspaceInvitationDto":{"type":"object","properties":{"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"nodeId":{"type":"string"},"roleId":{"type":"string"}},"required":["email","nodeId","roleId"]},"WorkspaceAssignmentResponseDto":{"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"}},"required":["id","identity_id","application_node_id","role_id","created_at"]},"CreateWorkspaceAssignmentDto":{"type":"object","properties":{"identityId":{"type":"string","description":"Identity to grant the role to"},"roleId":{"type":"string","description":"Identity role to grant"},"nodeId":{"type":"string","description":"Hierarchy node the role is granted at"},"effectiveFrom":{"type":"string","description":"When the assignment becomes effective"},"effectiveTo":{"type":"string","description":"When the assignment expires"}},"required":["identityId","roleId","nodeId"]},"UpdateWorkspaceAssignmentDto":{"type":"object","properties":{"nodeId":{"type":"string"},"roleId":{"type":"string"},"effectiveFrom":{"type":"string","nullable":true,"description":"When access begins; null clears it so access starts now"},"effectiveTo":{"type":"string","nullable":true,"description":"When access ends; null clears it so access never expires"}}},"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"]}}}}