{"openapi":"3.0.0","paths":{"/.well-known/openid-configuration":{"get":{"description":"OIDC discovery document (`GET /.well-known/openid-configuration`). Returns the unwrapped provider metadata: the `issuer`, the authorization, token, userinfo, JWKS, and revocation endpoints, and the supported capabilities — `response_types_supported` (`code`), `grant_types_supported` (`authorization_code`, `refresh_token`), `id_token_signing_alg_values_supported` (`RS256`), `scopes_supported` (`openid`, `profile`, `email`, `org`, `permissions`), and `code_challenge_methods_supported` (`S256`).","operationId":"DiscoveryController_getOpenIdConfiguration","parameters":[],"responses":{"200":{"description":""}},"summary":"OIDC discovery document","tags":["oidc"]}},"/.well-known/jwks.json":{"get":{"description":"JSON Web Key Set endpoint (`GET /.well-known/jwks.json`). Returns the public `RS256` signing keys (unwrapped, per RFC 7517) that relying parties use to verify issued `id_token` and access-token signatures.","operationId":"DiscoveryController_getJwks","parameters":[],"responses":{"200":{"description":""}},"summary":"JSON Web Key Set","tags":["oidc"]}},"/oauth/authorize":{"get":{"description":"OAuth2/OIDC authorization endpoint (`GET /oauth/authorize`). Validates `client_id`, `redirect_uri`, `response_type` (`code` only), and `code_challenge_method` (`S256` only) against the client's registered values, then serves the hosted login page bound to the client's Environment. When the Environment has a default end-user SSO connection the browser is redirected straight to the IdP; `?method=code` opens the passwordless email-OTP view. Returns `400` with an inline error page on invalid parameters.","operationId":"OAuthController_authorize","parameters":[{"name":"client_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"redirect_uri","required":true,"in":"query","schema":{"type":"string"}},{"name":"response_type","required":true,"in":"query","schema":{"type":"string"}},{"name":"scope","required":true,"in":"query","schema":{"type":"string"}},{"name":"state","required":true,"in":"query","schema":{"type":"string"}},{"name":"code_challenge","required":true,"in":"query","schema":{"type":"string"}},{"name":"code_challenge_method","required":true,"in":"query","schema":{"type":"string"}},{"name":"nonce","required":true,"in":"query","schema":{"type":"string"}},{"name":"method","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"OAuth2 authorization endpoint","tags":["oauth"]},"post":{"operationId":"OAuthController_authorizePost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAuthorizeDto"}}}},"responses":{"200":{"description":""}},"tags":["oauth"]}},"/oauth/login/email-otp/start":{"post":{"description":"Step 1 of the hosted passwordless email-OTP login: emails a one-time login code for the supplied `email`. Gated per-Environment on `email_otp_login_enabled` and (when enabled) `login_require_captcha` verification of `cf-turnstile-response`. Anti-enumeration — always renders a generic \"code sent\" page whether or not the identity exists; only a disabled-method or failed-CAPTCHA case re-renders the form with an error. Throttled to 5 requests per 15 minutes.","operationId":"OAuthController_loginEmailOtpStartPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthLoginEmailOtpStartDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: email a passwordless login code","tags":["oauth"]}},"/oauth/login/email-otp/verify":{"post":{"description":"Step 2 of the hosted passwordless email-OTP login: verifies the emailed `code`. On success it runs the shared MFA gate (trusted-device short-circuit, then challenge / enrollment / grace as required) with primary `amr` of `[\"otp\"]`, mints an authorization code, and consumes the single-use login code. On failure it re-renders the code-entry view with a generic error. Throttled to 10 requests per 10 minutes.","operationId":"OAuthController_loginEmailOtpVerifyPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthLoginEmailOtpVerifyDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: verify the passwordless login code","tags":["oauth"]}},"/oauth/register":{"get":{"description":"Hosted self-signup page, shown only when the client's Environment enables `allow_self_signup` (otherwise the login page is rendered instead). Carries the OAuth authorize parameters through so the flow resumes after the new identity verifies its email. On a passwordless (code-only) Environment it renders the email-OTP view, which self-provisions the identity on first code verification, rather than a password-bearing register form.","operationId":"OAuthController_registerPage","parameters":[{"name":"client_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"redirect_uri","required":true,"in":"query","schema":{"type":"string"}},{"name":"response_type","required":true,"in":"query","schema":{"type":"string"}},{"name":"scope","required":true,"in":"query","schema":{"type":"string"}},{"name":"state","required":true,"in":"query","schema":{"type":"string"}},{"name":"code_challenge","required":true,"in":"query","schema":{"type":"string"}},{"name":"code_challenge_method","required":true,"in":"query","schema":{"type":"string"}},{"name":"nonce","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"OAuth hosted login: self-signup page","tags":["oauth"]},"post":{"description":"Hosted self-signup submission. The underlying `register` call hard-gates on `allow_self_signup`, `self_signup_require_captcha`, the daily signup cap, and the origin check, and is anti-enumeration. On success the new identity is unverified, so the login page is rendered with a \"check your email\" notice and the OAuth parameters preserved; a gate failure re-renders the signup form with the error. Throttled to 10 requests per 15 minutes.","operationId":"OAuthController_registerPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthRegisterDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: self-signup form submission","tags":["oauth"]}},"/oauth/mfa-challenge":{"post":{"description":"Submits a TOTP code or recovery code from the hosted MFA-challenge page, identified by the `challenge_token` issued during login. On successful verification it optionally issues a trusted-device cookie (when `remember_device` is set and the Environment allows it), mints an authorization code, and redirects to the client's `redirect_uri`; on failure it re-renders the challenge page with the error and the available factor list. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaChallengePost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaChallengeDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: submit MFA challenge","tags":["oauth"]}},"/oauth/mfa/email-otp/send":{"post":{"description":"Step 1 of the hosted email-OTP MFA factor: emails a one-time code to the identity's verified address for the active `challenge_token`, then re-renders the challenge page in its \"code sent\" state with the OAuth parameters replayed. Stays on the \"email me a code\" view with an error if the send fails. Throttled to 3 requests per minute.","operationId":"OAuthController_mfaEmailOtpSendPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaEmailOtpSendDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: email a one-time MFA code","tags":["oauth"]}},"/oauth/mfa/email-otp/verify":{"post":{"description":"Step 2 of the hosted email-OTP MFA factor: verifies the emailed `code` against the `challenge_token`. On success it optionally issues a trusted-device cookie, mints an authorization code, and redirects to `redirect_uri`; on failure it re-renders the challenge page in the \"code sent\" state with the error. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaEmailOtpVerifyPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaEmailOtpVerifyDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: verify the emailed MFA code","tags":["oauth"]}},"/oauth/mfa/webauthn/options":{"post":{"description":"Step 1 of the hosted WebAuthn MFA factor: returns the assertion options (as raw JSON for the browser's `navigator.credentials.get()`) for the active `challenge_token`. Responds with `400` and `{ error }` when the challenge is invalid or expired. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaWebAuthnOptionsPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaWebAuthnOptionsDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: WebAuthn authentication options","tags":["oauth"]}},"/oauth/mfa/webauthn/verify":{"post":{"description":"Step 2 of the hosted WebAuthn MFA factor: verifies the browser assertion against the `challenge_token`. On success it optionally issues a trusted-device cookie, stamps the hosted-session cookie, mints an authorization code, and returns `{ redirect_to }` as JSON for the fetch-driven ceremony to navigate to; on failure it returns `400` with `{ error }`. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaWebAuthnVerifyPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaWebAuthnVerifyDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: verify a WebAuthn assertion","tags":["oauth"]}},"/oauth/mfa/webauthn/enroll/options":{"post":{"description":"Step 1 of hosted WebAuthn MFA enrollment: returns the attestation options (and a fresh `enrollment_token`) as JSON for the browser's `navigator.credentials.create()`. The subject is derived from the `enrollment_token` minted by the prior authorize step. Responds with `400` and `{ error: \"expired\" }` when that token is invalid or expired. Throttled to 5 requests per minute.","operationId":"OAuthController_mfaWebAuthnEnrollOptionsPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaWebAuthnEnrollOptionsDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: WebAuthn enrollment options","tags":["oauth"]}},"/oauth/mfa/webauthn/enroll/verify":{"post":{"description":"Step 2 of hosted WebAuthn MFA enrollment: verifies the attestation and persists the factor. When this is the identity's first factor the freshly-minted recovery codes plus the OAuth-resume context are sealed and `{ redirect_to }` points at the sealed-codes view so the codes can be shown; when a factor already exists it stamps the hosted session and mints the authorization code directly, returning `{ redirect_to }`. Returns `400` with `{ error }` on an invalid client or expired token. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaWebAuthnEnrollVerifyPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaWebAuthnEnrollVerifyDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: verify a WebAuthn enrollment attestation","tags":["oauth"]}},"/oauth/mfa-enroll-codes":{"get":{"description":"Server-renders the one-shot recovery-codes page from a sealed `codes_token`. This is the seam for the WebAuthn enroll-verify fetch flow, which cannot render the codes page itself, so it seals the codes plus OAuth-resume context and navigates here; the unseal also produces the acknowledgement envelope used by the subsequent codes-ack POST. Renders an expired-session error page when the token is missing or stale.","operationId":"OAuthController_mfaEnrollCodesView","parameters":[{"name":"codes_token","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"OAuth hosted login: view sealed enrollment recovery codes (WebAuthn fetch-flow seam)","tags":["oauth"]}},"/oauth/mfa-enroll-verify":{"post":{"description":"Verifies the 6-digit TOTP code from the hosted MFA enrollment page against the `enrollment_token`. On success it renders the one-shot recovery-codes page (whose acknowledgement step mints the authorization code); on failure it restarts the enrollment and re-renders the enroll page with a fresh `otpauth` URI and the error. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaEnrollVerifyPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaEnrollVerifyDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: verify MFA enrollment","tags":["oauth"]}},"/oauth/mfa-enroll-codes-ack":{"post":{"description":"Final step of hosted MFA enrollment: the user acknowledges they saved their recovery codes. Validates the sealed `ack_token` (state `verified`, matching `client_id`), then mints the authorization code — stamping the OIDC `amr` from the enrolled factor — and redirects to the client's `redirect_uri`. Renders an expired-session error page when the token is invalid. Throttled to 10 requests per minute.","operationId":"OAuthController_mfaEnrollCodesAckPost","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthMfaEnrollCodesAckDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth hosted login: acknowledge recovery codes","tags":["oauth"]}},"/oauth/token":{"post":{"description":"OAuth2 token endpoint (`POST /oauth/token`). Supports two grants: `authorization_code` (requires `code`, `redirect_uri`, and `code_verifier`, validates the confidential client's `client_secret` and the PKCE `S256` challenge, enforces single-use of the code and active Environment membership, then returns `access_token`, `id_token`, `token_type`, `expires_in`, and a rotating `refresh_token`) and `refresh_token` (requires `refresh_token`, `client_id`, and `client_secret`). Errors are returned in OAuth form — `invalid_request`, `invalid_grant`, or `unsupported_grant_type` — with a `400` status.","operationId":"OAuthController_token","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenDto"}}}},"responses":{"200":{"description":""}},"summary":"OAuth2 token exchange","tags":["oauth"]}},"/oauth/revoke":{"post":{"description":"OAuth2 token revocation endpoint (`POST /oauth/revoke`). Revokes the supplied `token`, invalidating the associated refresh token. Always responds `204 No Content`.","operationId":"OAuthController_revoke","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthRevokeDto"}}}},"responses":{"200":{"description":"Revoked. RFC 7009 answers 200 for an unrecognised token too, so a client cannot use this endpoint to learn whether a token exists or whose it is."},"401":{"description":"Client authentication failed. Send `client_secret_basic` (an `Authorization: Basic` header) or `client_secret_post` (client_id and client_secret in the body)."}},"summary":"Revoke a token","tags":["oauth"]}},"/oauth/userinfo":{"get":{"description":"OIDC UserInfo endpoint (`GET /oauth/userinfo`). Requires a valid identity access token (`IdentityJwtGuard`) and returns the standard OIDC claims for the authenticated subject: `sub`, `email`, `email_verified`, `given_name`, `family_name`, and `name`. Returns `401` for an invalid token and `403` when the bearer is not an identity principal.","operationId":"OAuthController_userinfo","parameters":[],"responses":{"401":{"description":"Invalid or expired token"},"403":{"description":"This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)"}},"summary":"OIDC UserInfo endpoint","tags":["oauth"]}}},"info":{"title":"Canopy OAuth 2.0 / OIDC","description":"Authorization Code + PKCE, token exchange, UserInfo, and discovery. Responses follow the OAuth2/OIDC specifications, not the Canopy response envelope.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"schemas":{"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"]},"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"]}}}}