{"openapi":"3.0.0","info":{"title":"Pria Runtime API","version":"2.0.1","description":"Pria API Documentation Praxis's developer platform is a core part of our mission to empower organizations to grow better. Our APIs are designed to enable teams of any shape or size to build robust integrations that help them customize and get the most value out of Pria. All Pria APIs are built using REST conventions and designed to have a predictable URL structure. <br/>  <br/>They use many standard HTTP features, including methods (POST, GET, PUT, DELETE) and error response codes.  <br/> <br/>All API calls are made under https://hiimpria.ai/api and all responses return standard JSON. In these docs, you'll find lists of all available endpoints for a given API, along with interactive code blocks for building requests. For walkthroughs of basic usage for these APIs, check out the API guides."},"servers":[{"url":"https://pria.praxislxp.com","description":"Pria Runtime API Server"}],"paths":{"/api/test/health":{"get":{"summary":"Check status of the middleware application","description":"Returns the current health status of the service including uptime, memory usage, and dependency status","responses":{"200":{"description":"Service is healthy and operational","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"status":"ok","uptime":112.368138,"timestamp":1750534150795,"summary":"Event loop lag: 12ms | RSS: 280MB | heap: 158MB/170MB | mongo: connected","eventLoopLag":{"meanMs":1.2,"p50Ms":0.4,"p95Ms":6.1,"p99Ms":15,"maxMs":42},"mongo":{"state":1,"label":"connected"},"memory":{"rss":128008192,"heapTotal":72105984,"heapUsed":67442352,"external":23693540,"arrayBuffers":19882176,"limitBytes":17179869184,"rssRatio":0.0075},"dependencies":{"database":1}}}}}},"tags":["Testing"]}},"/api/test/url":{"post":{"summary":"Retrieves an institution and validates user access for a specific context","description":"Use this method to determine if your originating URL or institution ID is associated with an institution,\nand optionally validate user access status. The properties returned can be used for branding your AI application.\n\n**Rate Limiting:** 30 requests per minute per IP address.\n\n**Lookup Priority:**\n1. First attempts to find institution by matching `url` against registered LTI context IDs\n2. If not found, falls back to direct lookup by `institutionid` (publicId)\n\n**User Validation (optional):**\nWhen `email` or `userid` is provided, the endpoint also validates:\n- User account status (must be 'active')\n- User-Institution membership status (must be 'active')\n\n**Status Values:**\n- `active`: Institution/user is active and accessible\n- `inactive`: Institution or user account is inactive\n- `pending`: User membership is pending approval\n- `suspended`: User membership is temporarily suspended\n- `invalid`: No institution found matching the provided criteria\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestUrlRequest"},"examples":{"ltiLookup":{"summary":"LTI Context URL lookup","value":{"url":"https://domain.edu/course/12345"}},"institutionLookup":{"summary":"Direct institution lookup","value":{"institutionid":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"}},"fullValidation":{"summary":"Full user validation","value":{"url":"https://domain.edu/course/12345","email":"student@domain.edu"}}}}}},"responses":{"200":{"description":"URL test completed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestUrlResponse"},"examples":{"activeInstitution":{"summary":"Active institution found","value":{"ltiassigned":true,"status":"active","name":"Acme University","ainame":"Hugo","picture":"https://storage.example.com/logo.png","publicId":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"}},"suspendedUser":{"summary":"User membership suspended","value":{"ltiassigned":true,"status":"suspended","name":"Acme University","ainame":"Hugo","picture":"https://storage.example.com/logo.png","publicId":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"}},"notFound":{"summary":"No institution found","value":{"ltiassigned":false,"status":"invalid"}}}}}},"400":{"description":"Bad request - invalid URL format"},"500":{"description":"Internal server error"}},"tags":["Testing"]}},"/api/agent/branding":{"post":{"summary":"Retrieve branding information for a digital twin","description":"Gets branding details for an institution's digital twin instance.\nUse this API to personalize your embedded application with the institution's\ncustom avatar, background images, AI name, and description.\n\n**No authentication required** when providing a valid `publicId`.\n\n## Use Cases\n- SDK/widget embedding customization\n- White-label application theming\n- Voice-enabled digital twin configuration\n","tags":["Branding"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBrandingRequest"},"example":{"publicId":"5f03e5e6-9dde-4ddc-b686-7a77ac617e52"}}}},"responses":{"200":{"description":"Branding information retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentBrandingResponse"},"example":{"picture":"https://pria.praxislxp.com/logo192_v3.webp","picture_animated":"","picture_bg":"https://pria.praxislxp.com/pria_bg2.webp","picture_dark_bg":"","elevenlabs_agent_id":"agent_0201kedjfdgee87bjke8acq3pcbg","about":"I am an AI-powered virtual mentor...","ainame":"Pria","status":"active"}}}},"404":{"description":"Digital twin not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/auth/signup":{"post":{"summary":"Register a new user","description":"Creates a new user account with email and password authentication.\n\n**Rate Limiting:** 5 requests per minute per IP address.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}},"responses":{"200":{"description":"User registered and authenticated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"400":{"description":"Bad request - Invalid email, password too short, or user already exists","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"User already exists"}}}}}}}}},"/api/auth/generateResetCode":{"post":{"summary":"Generate password reset code","description":"Sends a password reset code to the user's email address.\n\n**Rate Limiting:** 3 requests per minute per IP address.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResetCodeRequest"}}}},"responses":{"200":{"description":"Reset code generated and sent successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResetCodeResponse"}}}},"400":{"description":"Bad request - Invalid email"},"404":{"description":"User not found"}}}},"/api/auth/checkResetCode":{"post":{"summary":"Validate password reset code","description":"Validates whether the provided reset code is valid for the user.\n\n**Rate Limiting:** 10 requests per minute per IP address.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckResetCodeRequest"}}}},"responses":{"200":{"description":"Reset code validation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckResetCodeResponse"}}}},"400":{"description":"Bad request - Invalid or expired reset code"}}}},"/api/auth/changePassword":{"post":{"summary":"Change user password","description":"Changes the user's password using a valid reset code.\n\n**Rate Limiting:** 10 requests per minute per IP address.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}}},"responses":{"200":{"description":"Password changed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordResponse"}}}},"400":{"description":"Bad request - Invalid reset code or password too short"}}}},"/api/auth/checkActivateCode":{"post":{"summary":"Validate account activation code","description":"Validates whether the provided activation code is valid for the user.\n\n**Rate Limiting:** 10 requests per minute per IP address.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckActivateCodeRequest"}}}},"responses":{"200":{"description":"Activation code validation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckActivateCodeResponse"}}}},"400":{"description":"Bad request - Invalid or expired activation code"}}}},"/api/auth/sso/{id}":{"get":{"summary":"Initiate SSO login","description":"Initiates OAuth 2.0 SSO login for a specific provider","tags":["OAuth"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"SSO provider identifier"}],"responses":{"302":{"description":"Redirect to SSO provider"}}}},"/api/auth/sso_callback":{"post":{"summary":"SSO callback","description":"Handles the callback from OAuth 2.0 SSO authentication","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","description":"Authorization code"},"state":{"type":"string","description":"State parameter"}}}}}},"responses":{"200":{"description":"Authentication successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}}}}},"/api/auth/getInstitutionsForContextid":{"post":{"summary":"Get institutions for LTI context","description":"Retrieves institutions associated with a specific LTI context ID","tags":["Institutions"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetInstitutionsForContextRequest"}}}},"responses":{"200":{"description":"Institutions retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetInstitutionsForContextResponse"}}}}}}},"/api/auth/github/webhook":{"post":{"summary":"GitHub Marketplace webhook receiver","description":"Receives `marketplace_purchase` events from GitHub when an org or user\npurchases, upgrades, downgrades, or cancels the Pria Marketplace listing.\nThe endpoint validates the request via the `X-Hub-Signature-256` HMAC header\n(computed by GitHub with `GITHUB_WEBHOOK_SECRET`), then dispatches by `action`\n(`purchased`, `changed`, `cancelled`, `pending_change`, `pending_change_cancelled`).\n\nOn a new purchase the handler auto-creates the Pria user (via `autosignup`),\ngenerates a one-time login link (`?otp=<base64>` in the URL since the link is\ndelivered by email — see `docs/index/oauth-otp.md`), and emails it to the\npurchaser.\n\n**Authentication:** none in the traditional sense — request authenticity is\nproven by the `X-Hub-Signature-256` HMAC, compared with `crypto.timingSafeEqual`.\nRequests with a missing or wrong signature are rejected with 401. Application\nerrors that occur *after* signature validation still return 200 so GitHub does\nnot retry; the failure is surfaced via internal email instead.\n","tags":["OAuth"],"parameters":[{"in":"header","name":"X-Hub-Signature-256","required":true,"schema":{"type":"string"},"description":"HMAC-SHA256 of the raw request body, prefixed with `sha256=`.","example":"sha256=a1b2c3d4..."},{"in":"header","name":"X-GitHub-Event","schema":{"type":"string"},"description":"GitHub event type. Only `marketplace_purchase` is acted on; everything else is logged and acknowledged.","example":"marketplace_purchase"},{"in":"header","name":"X-GitHub-Delivery","schema":{"type":"string"},"description":"Per-delivery UUID assigned by GitHub (for log correlation)."}],"requestBody":{"required":true,"description":"Raw GitHub Marketplace payload. The handler reads `req.body` as a Buffer and\nJSON-parses after signature verification. See GitHub's marketplace_purchase\ndocumentation for the full shape.\n","content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["purchased","changed","cancelled","pending_change","pending_change_cancelled"],"example":"purchased"},"effective_date":{"type":"string","format":"date-time"},"sender":{"type":"object","properties":{"login":{"type":"string"},"id":{"type":"integer"},"type":{"type":"string"},"email":{"type":"string","nullable":true},"avatar_url":{"type":"string"}}},"marketplace_purchase":{"type":"object","properties":{"account":{"type":"object"},"plan":{"type":"object"},"billing_cycle":{"type":"string"},"unit_count":{"type":"integer"},"on_free_trial":{"type":"boolean"},"free_trial_ends_on":{"type":"string","nullable":true},"next_billing_date":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Webhook accepted. The handler always returns 200 once the signature passes\n— even if the downstream business logic threw — so GitHub does not retry.\nOn internal failures the response includes `error`.\n","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean","example":true},"error":{"type":"string","description":"Present only when post-signature processing threw."}}}}}},"401":{"description":"Missing `X-Hub-Signature-256` header, or the signature does not match\n`HMAC_SHA256(GITHUB_WEBHOOK_SECRET, body)`.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Invalid signature"}}}}}}}}},"/api/auth/google/services/authorize":{"get":{"summary":"Start the Google Services (per-user) OAuth consent flow","description":"Begins a Google OAuth 2.0 authorization-code flow scoped to the per-user services\nthe caller requests (Gmail / Drive / Calendar / Sheets / Docs). Generates a CSRF\nstate, stores it in `req.session.oauth_state` along with the caller's `userId`\nand institution context, then 302-redirects to Google's consent screen with\n`access_type=offline` and `prompt=consent` to ensure a refresh token is issued.\n\n**Storage routing (Option B):**\n- If the JWT has an institution attached, the resulting token is stored on\n  `UserInstitution.googleLoginToken` (institution-specific).\n- If there is no institution, it goes on `User.googleLoginToken` (personal).\n\n**Origin handling:** the `origin` query param (`profile` default, `chat` for the\nin-chat consent prompt) is preserved in the session so the post-callback redirect\nreturns the user to the right place.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"scopes","required":true,"schema":{"type":"string"},"description":"Comma-separated list of Google service names to request. Each name is\nexpanded via `GoogleServicesConfig.buildScopes`. Common values:\n`gmail`, `drive`, `calendar`, `sheets`, `docs`.\n","example":"gmail,calendar"},{"in":"query","name":"origin","schema":{"type":"string","enum":["profile","chat"],"default":"profile"},"description":"Where the user was when they triggered the consent. Controls the post-callback\nredirect — `chat` returns to the chat page, `profile` returns to the profile page.\n"}],"responses":{"302":{"description":"Redirect to Google's OAuth consent screen."},"400":{"description":"`scopes` is missing or empty.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"No services specified"}}}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."}}}},"/api/auth/google/services/callback":{"get":{"summary":"Google Services OAuth callback","description":"Google redirects here after the user accepts (or rejects) the consent screen.\nValidates the CSRF `state` against `req.session.oauth_state`, exchanges the\nauthorization `code` for tokens, then stores them on `UserInstitution.googleLoginToken`\nor `User.googleLoginToken` depending on the institution context captured at\nauthorize time.\n\nOn success the user is redirected to `${PRIA_URL}/my-profile/me?oauth=google&status=success`\n(profile origin) or `${PRIA_URL}/pria/personal/qanda` (chat origin). On failure the user\nis redirected to `${PRIA_URL}/oauth/success?error=<code>` where the SPA renders the\nerror. This callback is **not** a JWT-protected endpoint — auth is proven by the\nsession-bound CSRF state, identical to the public OAuth login callbacks.\n","tags":["OAuth"],"parameters":[{"in":"query","name":"code","schema":{"type":"string"},"description":"Authorization code returned by Google. Required on success."},{"in":"query","name":"state","schema":{"type":"string"},"description":"CSRF state to compare against `req.session.oauth_state.value`."},{"in":"query","name":"error","schema":{"type":"string"},"description":"Set by Google when the user denies consent or the request is malformed."}],"responses":{"302":{"description":"Always a redirect. On success → profile or chat page (per the captured `origin`).\nOn any failure (missing code, state mismatch, token exchange error) →\n`${PRIA_URL}/oauth/success?error=<code>`.\n"}}}},"/api/auth/google/services/validate":{"get":{"summary":"Validate the caller's stored Google Services token","description":"Checks whether the caller's stored Google access token is still accepted by Google\nby making a probing call to the userinfo endpoint. Resolves which storage location\nto read from in the same priority order as the runtime authenticator: institution\ntoken first (when the JWT has an institution), personal token second.\n\nIf Google rejects the token (revoked externally, expired refresh chain, etc.),\nthe handler clears it from the database and returns `valid: false, cleared: true`.\nThis is a side-effecting probe — calling it can wipe a stale token.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Validation completed. Inspect `valid` for the outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleOAuthValidateResponse"}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."},"404":{"description":"The caller's User record could not be loaded.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"User not found"}}}}}},"500":{"description":"Unexpected error during validation.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Validation request failed"}}}}}}}}},"/api/auth/google/services/revoke":{"delete":{"summary":"Revoke the caller's stored Google Services token","description":"Clears the caller's stored Google OAuth token from the database — institution-specific\n(`UserInstitution.googleLoginToken`) when the JWT has institution context, otherwise\npersonal (`User.googleLoginToken`). Best-effort: also calls Google's\n`https://oauth2.googleapis.com/revoke` to invalidate the token at Google's end. If\nthe upstream call fails (e.g. token already expired), the local database is still\ncleared and the endpoint still returns success.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Token cleared locally (Google upstream revoke is best-effort).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleOAuthRevokeResponse"}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."},"404":{"description":"The caller's User record could not be loaded.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"User not found"}}}}}},"500":{"description":"Unexpected error during revoke.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to revoke Google services access"}}}}}}}}},"/api/auth/google/institution/authorize":{"get":{"summary":"Start the Google OAuth flow for an institution-level token","description":"Begins an institution-scoped Google OAuth 2.0 authorization-code flow. The\nresulting token is later stored on `Institution.cloudServices.google.googleLoginToken`\nand is used as the **shared** institution credential for downstream Google API\ncalls performed on behalf of any member of that institution.\n\nThe caller MUST hold `institutions.edit` on the target institution (checked via\n`checkRAPForUI`). The captured session state records the connecting user's `_id`\n(`connectedByUserId`) so the callback can attribute the connection.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"institutionId","required":true,"schema":{"type":"string"},"description":"ObjectId of the institution to attach the token to.","example":"6631915765bb0a94cfd6ca99"},{"in":"query","name":"scopes","required":true,"schema":{"type":"string"},"description":"Comma-separated Google service names. Resolved via\n`GoogleServicesConfig.buildScopes`.\n","example":"gmail,drive,calendar"}],"responses":{"302":{"description":"Redirect to Google's OAuth consent screen. On error during initiation\n(configuration / lookup failure) the user is redirected to\n`${PRIA_URL}/oauth/success?error=auth_initiation_failed` instead.\n"},"400":{"description":"`institutionId` or `scopes` missing.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Institution ID required"}}}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."},"403":{"description":"Caller does not hold `institutions.edit` on the target institution.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Not authorized to configure this institution"}}}}}},"404":{"description":"Institution not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Institution not found"}}}}}}}}},"/api/auth/google/institution/{institutionId}/validate":{"get":{"summary":"Validate an institution's stored Google token","description":"Probes Google's userinfo endpoint with the institution's stored\n`cloudServices.google.googleLoginToken` to check whether it is still accepted.\nIf Google rejects the token, the entire `cloudServices.google` subtree is\ncleared (`$unset`) and the response reports `valid: false, cleared: true`.\nThe caller must hold `institutions.edit` on the target institution.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"institutionId","required":true,"schema":{"type":"string"},"description":"ObjectId of the institution whose token to validate."}],"responses":{"200":{"description":"Validation completed. Inspect `valid` for the outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"cleared":{"type":"boolean","description":"True when an invalid token was just unset."},"message":{"type":"string","description":"Set when no Google token is configured for the institution.","example":"No Google tokens configured"},"error":{"type":"string","description":"Set on validation failure (passes through Google's error)."}}}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."},"403":{"description":"Caller does not hold `institutions.edit`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not authorized"}}}}}},"404":{"description":"Institution not found.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Institution not found"}}}}}},"500":{"description":"Unexpected error during validation.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Validation request failed"}}}}}}}}},"/api/auth/google/institution/{institutionId}/revoke":{"delete":{"summary":"Revoke an institution's stored Google token","description":"Clears `cloudServices.google` from the target institution (`$unset`), and\nbest-effort revokes the access token at Google. The caller must hold\n`institutions.edit`. As with the per-user revoke, an upstream revoke failure\ndoes not prevent the local cleanup or change the 200 response.\n","tags":["OAuth"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"institutionId","required":true,"schema":{"type":"string"},"description":"ObjectId of the institution to disconnect."}],"responses":{"200":{"description":"Institution OAuth config cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleOAuthRevokeResponse"}}}},"401":{"description":"Missing or invalid JWT (verifyToken)."},"403":{"description":"Caller does not hold `institutions.edit`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not authorized"}}}}}},"404":{"description":"Institution not found.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Institution not found"}}}}}},"500":{"description":"Unexpected error during revoke.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to revoke institution Google services access"}}}}}}}}},"/api/auth/signin":{"post":{"summary":"User authentication and sign-in","description":"Authenticates a user with email and password, and returns a JWT token along with the user profile.\n\n**Rate Limiting:** 10 requests per minute per IP address.\n\n## JWT Token Lifecycle\n\nOn successful authentication, the response includes a `token` field containing a signed JWT.\n\n**Token payload:**\n- `_id` — User's unique identifier\n- `email` — User's email address\n- `customerId` — Stripe customer ID (if applicable)\n- `accountType` — One of `super`, `admin`, or `user`\n- `sessionId` — Server-side session identifier\n- `iat` — Issued-at timestamp (set automatically by JWT)\n- `exp` — Expiration timestamp (set automatically by JWT)\n\n**Token expiration:** 6 hours (21,600 seconds) by default. Configurable via `JWT_VALIDITY_SEC` environment variable.\n\n## Using the Token\n\nInclude the JWT in every subsequent API request using one of these methods (in priority order):\n\n1. **`x-access-token` header (recommended):**\n   ```\n   x-access-token: eyJhbGciOiJIUzI1NiIs...\n   ```\n\n2. **`Authorization` header with Bearer scheme:**\n   ```\n   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...\n   ```\n\n3. **Query parameter:**\n   ```\n   GET /api/resource?token=eyJhbGciOiJIUzI1NiIs...\n   ```\n\n4. **Request body field:**\n   ```json\n   { \"token\": \"eyJhbGciOiJIUzI1NiIs...\" }\n   ```\n\n## Token Errors\n\nWhen a token is missing, expired, or invalid, the API returns:\n\n- **403** — No token provided (`Authentication Required`)\n- **401** — Token expired (`jwt expired`) or token invalid (`invalid signature`)\n\n## Token Renewal (Sliding Session)\n\nTokens are automatically refreshed via a sliding session mechanism. Each time the client calls\n`POST /api/user/refresh/profile`, the response includes a fresh JWT token with a new expiration.\nThis extends the session without requiring re-authentication, as long as the current token is still valid.\n\nThe frontend calls this endpoint on every page load, so active users never experience token expiration.\nIf the token expires (e.g., user is inactive for more than 6 hours), a new sign-in is required.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRequest"}}}},"responses":{"200":{"description":"Successful authentication. Returns JWT token and user profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Bad request - missing required or invalid fields","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Password must be at least 6 characters long"},"success":{"type":"boolean","example":false}}}}}},"401":{"description":"Invalid credentials or inactive account","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Invalid Password !"},"token":{"type":"string","nullable":true,"example":null}}}}}},"403":{"description":"Account not activated - an activation email has been sent","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Check your email for a one-time activation link to complete your account setup."}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"The system can not sign you in at this time."}}}}}}}}},"/api/auth/autosignup":{"post":{"summary":"Automatic user signup and authentication","description":"Creates or authenticates a user account automatically based on CMS/LMS integration data.\nTypically called after SDK launch token verification (see `/api/auth/sdk-verify`).\n\n**Security:**\n- Rate limited to 10 requests per minute per IP\n- All new SDK signups are assigned `accountType: \"user\"` regardless of `lxp_user_type`\n- Admin promotion is handled server-side via the institution's `contactEmail` trust anchor\n- URL validation uses exact hostname comparison (not substring matching)\n- Default passwords are cryptographically random (not derivable from email)\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoSignupRequest"}}}},"responses":{"200":{"description":"Successful authentication with user profile and token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoSignupResponse"}}}},"400":{"description":"Bad request - invalid input data"},"401":{"description":"Unauthorized - invalid or missing authentication token"},"500":{"description":"Internal server error"}},"tags":["Authentication"]}},"/api/auth/createInstitutionForContextid":{"post":{"summary":"Create a new institution (Digital Twin) for a specific context URL","description":"Creates a new institution record with the provided configuration and context information.\nThe new institution's parent account is resolved in order: the explicit `account` id when provided; otherwise the first account whose `domainUrls` contains the supplied `domain`; otherwise the parent account of the `contactEmail` user's own institution. If none match, the institution is created without a parent account.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInstitutionRequest"}}}},"responses":{"200":{"description":"Institution created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInstitutionResponse"}}}},"400":{"description":"Bad request - Invalid input data"},"401":{"description":"Unauthorized - Invalid or missing access token"},"500":{"description":"Internal server error"}},"tags":["Institutions"]}},"/api/auth/generate-prompt-preview":{"post":{"summary":"Generate Digital Twin instructions from Q&A","description":"Takes interview question/answer pairs and generates AI-powered persona instructions\nfor a Digital Twin being created. Used during the creation wizard before the\ninstitution exists. Requires JWT authentication.\n","tags":["Authentication"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeneratePromptPreviewRequest"}}}},"responses":{"200":{"description":"Generated instructions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeneratePromptPreviewResponse"}}}},"400":{"description":"Invalid request or generation error"},"401":{"description":"Authentication required"}}}},"/api/auth/sdk-sign":{"post":{"summary":"Sign SDK launch parameters","description":"Signs launch parameters with a server-held HMAC-SHA256 secret for secure SDK iframe embedding.\nCalled by `pria-sdk.js` before creating the launch iframe.\n\n**Security model:**\n- The signing secret (`SDK_LAUNCH_SECRET`) is never exposed to the client\n- Parameters are canonicalized (all values stringified, `launch_*` keys stripped) to ensure\n  consistent HMAC computation across sign and verify, since URL query strings coerce values to strings\n- A cryptographic nonce and timestamp are included to prevent replay attacks\n- The token expires after 10 minutes\n\n**Origin validation:**\n- For institution-specific launches, the request `Origin` or `Referer` header is validated\n  against the institution's `publicAuthorizedUrls` using exact hostname comparison\n- In development mode (`NODE_ENV !== 'production'`), `file://` origins (null/missing Origin header) are allowed\n\n**Digital twin selector mode:**\n- When `institutionId` is an empty string and `params.digitaltwin` is true, institution lookup\n  and origin validation are skipped. The user will be presented with their existing digital twins\n  or the option to create a new one.\n","tags":["SDK Launch"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SdkSignRequest"},"examples":{"institutionLaunch":{"summary":"Standard institution launch","value":{"params":{"email":"john.doe@domain.edu","profilename":"John Doe","usertype":4,"userid":110,"institutionid":"f831501f-b645-481a-9cbb-331509aaf8c1","task":"do"},"institutionId":"f831501f-b645-481a-9cbb-331509aaf8c1"}},"digitalTwinSelector":{"summary":"Digital twin selector (no specific institution)","value":{"params":{"email":"john.doe@domain.edu","profilename":"John Doe","usertype":4,"digitaltwin":true,"task":"do","institutionid":""},"institutionId":""}}}}}},"responses":{"200":{"description":"Parameters signed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SdkSignResponse"}}}},"400":{"description":"Missing params/institutionId or invalid institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid institution"}}}}}},"403":{"description":"Origin not authorized for this institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Unauthorized origin"}}}}}},"500":{"description":"Server configuration error (SDK_LAUNCH_SECRET not set)"}}}},"/api/auth/sdk-verify":{"post":{"summary":"Verify SDK launch token","description":"Verifies an HMAC-SHA256 launch token against the server-held secret.\nCalled by `Sdk.js` (React frontend) before proceeding to autosignup.\n\n**Verification steps:**\n1. Checks that the timestamp is within a 10-minute window\n2. Recomputes the HMAC from canonicalized params (values stringified, `launch_*` keys stripped)\n3. Compares using constant-time `crypto.timingSafeEqual` to prevent timing attacks\n\n**When verification fails:**\n- Expired tokens (>10 min) return 401 with \"Launch token expired\"\n- Tampered or invalid tokens return 401 with \"Invalid launch token\"\n","tags":["SDK Launch"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SdkVerifyRequest"}}}},"responses":{"200":{"description":"Token verified successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SdkVerifyResponse"}}}},"400":{"description":"Missing verification parameters","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Missing verification parameters"}}}}}},"401":{"description":"Token expired, invalid, or verification failed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid launch token"}}}}}},"500":{"description":"Server configuration error (SDK_LAUNCH_SECRET not set)"}}}},"/api/auth/api-key-signin":{"post":{"summary":"Exchange a Pria API key for a JWT","description":"Validates the `x-api-key` header against the hashed key stored on the user record\nand returns a regular Pria JWT (`token`) plus a minimal `profile` envelope. The\nJWT is identical in shape and lifetime to the one issued by `POST /api/auth/signin`\n— every other authenticated endpoint accepts it via `Authorization: Bearer <jwt>`\nor `x-access-token: <jwt>`.\n\n**Important: the API key is NOT a JWT.** Sending the raw `pria_…` key as a\n`Authorization: Bearer` value will fail with `Invalid access token jwt malformed`\non the bearer-protected endpoints. You must do the exchange here first.\n\n**Authentication transport:** the API key MUST be sent in the `x-api-key` header,\nnot `Authorization`. The endpoint has no JWT gate — only the key check.\n\n**Access gating:**\n- Key format is enforced: `pria_` followed by 40 hex chars (`/^pria_[0-9a-f]{40}$/`).\n  A malformed key returns 401, not 400.\n- Lookup uses the 9-char prefix for indexing, then verifies the full SHA-256 hash.\n- Only users with `accountType` of `admin` or `super` (and `status !== 'deleted'`)\n  can mint a JWT. Demoting a user immediately disables their key.\n\n**Rate limiting:** 100 requests per minute per IP (the shared auth limiter).\n","tags":["Authentication"],"security":[{"priaApiKey":[]}],"parameters":[{"in":"header","name":"x-api-key","required":true,"schema":{"type":"string","pattern":"^pria_[0-9a-f]{40}$"},"description":"Pria API key (`pria_` + 40 hex chars). Provisioned by an admin via the admin UI.","example":"pria_0d59f32058727e990bbd5cbdac7668dc2e2c6c09"}],"responses":{"200":{"description":"Key accepted — JWT and minimal profile returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeySigninResponse"}}}},"400":{"description":"`x-api-key` header is missing or not a string.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"x-api-key header is required"}}}}}},"401":{"description":"Key is malformed, unknown, or the bound user is not admin/super (or is deleted).\nThe handler returns the same generic message for all three cases to avoid\nleaking which keys exist.\n","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid API key"}}}}}},"429":{"description":"Too many requests (IP-level abuse limiter — 100/min)."},"500":{"description":"Internal server error during signin.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Sign-in failed: <error details>"}}}}}}}}},"/api/auth/token_complete":{"get":{"summary":"Complete the Canvas LMS API authorization handoff","description":"Canvas redirects the user's browser here after they authorize Pria to call the\nCanvas API on their behalf (Canvas OAuth2 authorization-code flow). The handler\nexchanges the supplied `code` for a Canvas API access token, stores it on the\ncaller's `UserInstitution.canvasApiToken`, writes a history entry recording the\nsuccessful authentication, and returns a small HTML page that closes the popup\nand signals the parent window to retry the request that triggered the auth.\n\nThis is **not** a Pria login flow — the user is already signed in with a valid\nPria JWT before being sent to Canvas. The `state` parameter carries the Pria\nuser's `_id` so the backend can look up which user to attach the Canvas token\nto without relying on the session.\n\n**Configuration requirements:** the user's institution (or its parent account)\nmust have `canvasClientId` and `canvasClientSecret` set, and a derivable Canvas\nAPI domain (an instructure.com URL or a `.edu` vanity URL in\n`publicAuthorizedUrls`).\n","tags":["OAuth"],"parameters":[{"in":"query","name":"code","schema":{"type":"string"},"required":true,"description":"Canvas OAuth authorization code to exchange for an access token."},{"in":"query","name":"state","schema":{"type":"string"},"required":true,"description":"Pria user `_id` — set during the original `consentUrl` generation so this\ncallback can find which Pria user to attach the Canvas token to.\n"},{"in":"query","name":"error","schema":{"type":"string"},"description":"Set by Canvas when the user denies the consent screen."},{"in":"query","name":"error_description","schema":{"type":"string"},"description":"Human-readable failure reason that accompanies `error`."}],"responses":{"200":{"description":"Canvas token stored successfully. Returns an HTML page (`TOKEN_SUCCESS_HTML`)\nthat closes the popup window and signals the opener to retry.\n","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"Canvas returned an OAuth error, required query parameters are missing,\nthe institution is misconfigured (no Canvas API URL / client id / secret),\nor the Canvas token exchange itself failed.\n","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Parameter code is required"},"error":{"type":"string","description":"Present on Canvas exchange failures (instead of `message`).","example":"Unknown error (3) <upstream>"}}}}}},"404":{"description":"User identified by `state` was not found, or the required UserInstitution\nentitlement row is missing.\n","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"User not found"}}}}}},"500":{"description":"Database error while loading the UserInstitution entitlement.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error - can't find the user's entitlement"}}}}}}}}},"/api/auth/mfa-verify":{"post":{"summary":"Submit a 6-digit MFA code to complete login","description":"Second leg of the MFA login flow. The first leg (POST /api/auth/signin\nor /autosignup) returned `{mfaRequired: true, challengeId, maskedEmail}`\nand emailed a 6-digit code. This endpoint accepts the typed code, and\non success returns the same shape as a normal signin response (token +\nprofile) and sets a long-lived `pria_mfa_trust` cookie that lets the\nsame browser skip MFA for the next 7 days (or whatever MFA_TRUST_DAYS\nsets, bounded to [1, 30]).\n\n**Brute-force protection:** 5 wrong attempts per code; the 6th burns\nthe challenge (status invalidated) and the user must request a new\ncode via /mfa-resend. The signin gate further locks the account for\n10 minutes after 5 invalidated challenges (with at least one wrong\nattempt each) in the previous 1 hour.\n\n**Rate limit:** 100 req/min per IP (authLimiter).\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyRequest"}}}},"responses":{"200":{"description":"Code accepted. Response shape matches POST /api/auth/signin success — a JWT token plus the user profile. A Set-Cookie pria_mfa_trust=... header is also set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"400":{"description":"Missing challengeId or code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"401":{"description":"Wrong code — `attemptsRemaining` indicates how many tries remain before the challenge is invalidated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"410":{"description":"Challenge unknown / expired / already verified / cancelled — client must restart the signin flow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"423":{"description":"Too many wrong attempts on this code; the challenge is invalidated. Request a fresh code via /mfa-resend.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}}}}},"/api/auth/mfa-resend":{"post":{"summary":"Request a new MFA code for the current challenge","description":"Invalidates the current challenge and issues a fresh one with the\nsame loginContext (so the verify step still lands you in the same\nbranding / institution context). The new challenge has its own 5-min\nTTL and its own 5-attempt cap.\n\n**Cooldown:** the previous challenge must be at least 30 seconds old\nbefore a Resend is allowed — prevents email-bombing.\n\nResend-driven invalidations (where the user never submitted a wrong\ncode) do NOT count toward the per-account 5-burned-codes-in-1h\nlockout cap.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeIdRequest"}}}},"responses":{"200":{"description":"New code emailed; client must use the newly-returned challengeId for subsequent verify / resend / cancel calls.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaResendResponse"}}}},"400":{"description":"Missing challengeId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"410":{"description":"Challenge unknown / expired / already verified / cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"429":{"description":"Resend cooldown active — `retryAfter` (seconds) indicates the remaining wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}},"502":{"description":"Email provider failure — challenge is invalidated; client should restart the signin flow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}}}}},"/api/auth/mfa-cancel":{"post":{"summary":"Abandon the current MFA challenge (user clicked Cancel)","description":"Marks the challenge as invalidated server-side so it can no longer be\nused for /mfa-verify or /mfa-resend. Idempotent — calling it on an\nalready-cancelled / expired / verified challenge still returns 200.\n\nThe client should clear its `sessionStorage` copy of the challengeId\nand route the user back to the login screen.\n","tags":["Authentication"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaChallengeIdRequest"}}}},"responses":{"200":{"description":"Cancellation acknowledged.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true}}}}}},"400":{"description":"Missing challengeId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MfaVerifyError"}}}}}}},"/api/user/credit-cap-status":{"get":{"summary":"Credit-cap gauges and block state for the caller's current institution.","description":"Returns the caller's usage against any configured credit caps for their\ncurrent institution (and its parent account). `capsEnabled` is false when\ncaps are off or the user has no capped account; in that case the gauges\ncarry null caps. Reuses the same evaluator as server-side enforcement, so\nthe UI and the hard block agree exactly.\n","tags":["User"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Cap status for the caller.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"capsEnabled":{"type":"boolean"},"blocked":{"type":"boolean"},"capType":{"type":"string","nullable":true,"enum":["h24","institution","account",null]},"source":{"type":"string","enum":["account","institution","none"],"description":"Which tier currently owns the caps for this user's institution."},"perInstitution":{"type":"object","properties":{"used":{"type":"number"},"cap":{"type":"number","nullable":true}}},"h24":{"type":"object","properties":{"used":{"type":"number"},"cap":{"type":"number","nullable":true},"resetAt":{"type":"string","format":"date-time","nullable":true}}},"accountWide":{"type":"object","properties":{"used":{"type":"number"},"cap":{"type":"number","nullable":true}}}}}}}}}}},"/api/user/history/{id}/composition":{"get":{"summary":"Per-dialog context-composition breakdown for the InfoCard 10×10 grid.","description":"Returns the categorised input/output token split for one LLM dialog.\nBuilt asynchronously by a `process.nextTick` dispatch from\n`routes/logic/sendResponse.js` after `res.json` has flushed, so the\nbreakdown is typically available within ~50 ms of the dialog\ncompleting — but the InfoCard MUST handle the \"pending\" race window\nby retrying on `404 — composition pending`.\n\nInput categories (12 total) roll up into 3 ownership boundaries:\n  - **system**: persona, systemRules, toolDefinitions, toolInstructions, toolGuidance, formatting\n  - **twin**: digitalTwin, assistant, rag\n  - **user**: userContext, history, toolResults\n\nOutput categories (3 total): thinking, completion, toolCalls.\n\nPermission: caller must own the dialog (`History.user === req.user._id`),\nOR be `accountType === 'super'`. Dialogs flipped to `forgotten:true`\nreturn 404 (same behaviour as the rest of `/api/user/history/:id/*`).\n","tags":["User"],"security":[{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The History row's `_id`.","schema":{"type":"string","pattern":"^[a-f0-9]{24}$"}}],"responses":{"200":{"description":"Composition breakdown.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"composition":{"type":"object","properties":{"dialog":{"type":"string","description":"The History row _id."},"user":{"type":"string"},"institution":{"type":"string","nullable":true},"conversationModel":{"type":"string","example":"us.anthropic.claude-sonnet-4-6"},"provider":{"type":"string","example":"bedrock"},"tokenizer":{"type":"string","example":"estimate"},"input":{"type":"object","properties":{"totalTokens":{"type":"integer"},"cacheBreakpointAfterCategory":{"type":"string","nullable":true,"example":"assistant"},"sections":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string","enum":["toolDefinitions","toolInstructions","persona","systemRules","toolGuidance","formatting","digitalTwin","assistant","rag","history","toolResults","userContext"]},"boundary":{"type":"string","enum":["system","twin","user"]},"label":{"type":"string"},"tokens":{"type":"integer"},"stable":{"type":"boolean"}}}}}},"output":{"type":"object","properties":{"totalTokens":{"type":"integer"},"partial":{"type":"boolean"},"sections":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string","enum":["thinking","completion","toolCalls"]},"label":{"type":"string"},"tokens":{"type":"integer"}}}}}},"computeDurationMs":{"type":"integer"},"schemaVersion":{"type":"integer","example":1},"createdAt":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Invalid history id."},"401":{"description":"Authentication required."},"404":{"description":"Either the dialog does not exist / caller does not own it / dialog is\nforgotten (`{ success:false, message:'History not found' }`), OR the\ncomposition document has not been written yet\n(`{ success:false, message:'composition pending' }`). The InfoCard\ntreats the latter as \"retry shortly\".\n"}}}},"/api/user/favoriteAssistants/{id}":{"post":{"summary":"Add an assistant to the caller's favorites","description":"Appends the given assistant ObjectId to the authenticated user's `favoriteAssistants` list. Idempotent — if the id is already present, the list is returned unchanged and no save is performed. Consumed by the Pria input-pill Assistants submenu, which reads the populated favorites from `/api/user/refresh/profile`.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to favorite","example":"687e761093c797174459c651"}],"responses":{"200":{"description":"Favorite added (or already present — list returned)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"favoriteAssistants":{"type":"array","description":"Updated list of favorite assistant ObjectIds","items":{"type":"string"}}}}}}},"400":{"description":"Invalid assistant id (not a valid ObjectId)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid assistant id"}}}}}},"401":{"description":"Unauthorized — user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication required"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}}}},"delete":{"summary":"Remove an assistant from the caller's favorites","description":"Removes the given assistant ObjectId from the authenticated user's `favoriteAssistants` list. Tolerant — the id does NOT need to be currently favorited; calling DELETE on a non-favorited id (or a malformed id) is a no-op that still returns 200 with the current list.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to unfavorite","example":"687e761093c797174459c651"}],"responses":{"200":{"description":"Favorite removed (or absent — list returned)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"favoriteAssistants":{"type":"array","description":"Updated list of favorite assistant ObjectIds","items":{"type":"string"}}}}}}},"401":{"description":"Unauthorized — user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication required"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}}}}},"/api/user/files/search-content":{"post":{"summary":"Search file content via RAG-KAG and return scored snippets","description":"Scored-chunk content search across the user's eligible IP Vault\n(personal + current institution + account-shared siblings). Returns\n~200-char snippets centered on matched tokens, with a relevance score\nper chunk and a `rag` | `kag` | `fused` source label. Confidential\ninstitution files surface a 🔒 placeholder snippet rather than raw\nchunk text. `selectedUploadIds` is optional — when provided, it\nintersects with the user's scope; foreign or malformed ids are\nsilently dropped (never 403). Rate-limited per user (default 20/min).\nResults now include KAG graph matches when KAG is enabled for a vault in\nscope, and each result carries the full `content` plus KAG evidence\n(`matchedEntities`, `trace`) for inline expansion.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","maxLength":500,"description":"The search query string.","example":"quantum gravity"},"selectedUploadIds":{"type":"array","nullable":true,"maxItems":5000,"items":{"type":"string"},"description":"Optional whitelist of upload ids. Intersected with the user's eligible scope; foreign / malformed ids are silently dropped."},"minScore":{"type":"number","format":"float","minimum":0,"maximum":1,"default":0.1,"description":"Minimum dense (vector) score floor. Ignored for KAG-fused results."},"limit":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Maximum number of results to return."}}}}}},"responses":{"200":{"description":"Content-search results","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"query":{"type":"string","example":"quantum gravity"},"results":{"type":"array","items":{"type":"object","properties":{"uploadId":{"type":"string"},"chunkId":{"type":"string"},"chunkIndex":{"type":"integer"},"snippet":{"type":"string","description":"Plain-text snippet, ~200 chars, centered on the matched token. Confidential hits return `🔒 Private content matched — open the file to view`."},"content":{"type":"string","description":"Full matched chunk text for inline expansion. Confidential hits return the same 🔒 placeholder as `snippet`."},"matchedEntities":{"type":"array","items":{"type":"string"},"description":"KAG graph entity names that matched this hit. Empty unless the graph leg contributed (source kag/fused)."},"trace":{"type":"array","items":{"type":"object","properties":{"list":{"type":"string","enum":["dense","graph"]},"rank":{"type":"integer"}}},"description":"RRF provenance — which retriever(s) and at what rank produced this hit. Empty for dense-only results."},"score":{"type":"number","format":"float","description":"Relevance score (0..1 for dense, ~0.00..0.05 for RRF-fused)."},"source":{"type":"string","enum":["rag","kag","fused"]},"upload":{"type":"object","properties":{"_id":{"type":"string"},"originalname":{"type":"string"},"filesize":{"type":"integer"},"mimetype":{"type":"string"},"file_title":{"type":"string"},"institution":{"type":"string","nullable":true},"account_shared":{"type":"boolean","description":"True when the upload is shared at the account level (visible across sibling institutions)."},"is_private":{"type":"boolean"},"confidential":{"type":"boolean","description":"True when the upload's snippet is suppressed (other-user is_private institution file)."}}}}}},"totalScanned":{"type":"integer","description":"Number of raw results returned by searchRag before the minScore filter."},"uploadCount":{"type":"integer","description":"Number of distinct uploads in the user's searchable scope."},"tookMs":{"type":"integer","description":"Server-side elapsed time in milliseconds."}}}}}},"400":{"description":"Validation error (missing or oversized query, server error)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"429":{"description":"Rate limit exceeded (default 20 / minute / user)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Too many content searches. Try again in a moment."}}}}}}},"security":[{"apiKeyAuth":[]}],"tags":["RAG"]}},"/api/user/kag/personal/backfill-quote":{"get":{"summary":"Estimate cost of processing personal-vault files through KAG extraction","description":"Returns how many of the caller's personal-vault files (and their chunks) still need knowledge-graph extraction at the current extraction version, plus an estimated TOKEN cost (with a 1.2x safety buffer). Token→credit conversion is handled separately; this endpoint reports tokens, not credits.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Quote details","content":{"application/json":{"schema":{"type":"object","properties":{"unprocessedFiles":{"type":"integer","description":"Personal files with no completed KAG job yet"},"unprocessedChunks":{"type":"integer","description":"Total chunks across those files"},"estimatedTokens":{"type":"integer","description":"Estimated extraction tokens (includes a 1.2x safety buffer)"}}}}}},"500":{"description":"Server error"}}}},"/api/user/kag/personal/backfill":{"post":{"summary":"Start (or resume) KAG backfill for the caller's personal vault","description":"Enqueues knowledge-graph extraction jobs for the caller's eligible personal-vault files (one job per file, reusing the existing backfill machinery). Returns 409 if a personal backfill is already in progress. Backfill jobs run at a lower priority than live uploads.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Backfill enqueued","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"summary":{"type":"object","description":"Per-upload enqueue summary (created / requeued / alreadyDone counts)"}}}}}},"409":{"description":"A personal KAG backfill is already in progress","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"inFlight":{"type":"integer"}}}}}},"500":{"description":"Server error"}}}},"/api/user/kag/personal/backfill/status":{"get":{"summary":"Aggregate status of the caller's personal-vault KAG jobs","description":"Aggregates the caller's personal knowledge-graph jobs into a single progress view. `state` is idle when there are no jobs, otherwise running / queued / error / done derived across all jobs.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Aggregated status","content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string","enum":["idle","queued","running","error","done"]},"processedChunks":{"type":"integer"},"totalChunks":{"type":"integer"},"tokensIn":{"type":"integer"},"tokensOut":{"type":"integer"},"error":{"type":"string","nullable":true}}}}}},"500":{"description":"Server error"}}}},"/api/user/kag/personal/backfill/active":{"delete":{"summary":"Cancel the caller's in-progress personal KAG backfill","description":"Marks in-flight personal backfill jobs (lower-priority) as skipped. Live uploads and already-completed graph data are left intact.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Cancellation applied","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"cancelled":{"type":"integer","description":"Number of in-flight backfill jobs cancelled"}}}}}},"500":{"description":"Server error"}}}},"/api/user/institution/{id}/kag/backfill-quote":{"get":{"summary":"Estimate cost of processing a Digital Twin's files through KAG extraction","description":"Returns how many of the institution's (Digital Twin's) files still need knowledge-graph extraction at the current extraction version, plus an estimated TOKEN cost (with a 1.2x safety buffer). Authorized per-instance via `institutions.edit` (super bypasses) — NOT the global admin gate — so a per-institution admin can run it for the Twin they manage.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Institution (Digital Twin) id"}],"responses":{"200":{"description":"Quote details","content":{"application/json":{"schema":{"type":"object","properties":{"unprocessedFiles":{"type":"integer","description":"Files with no completed KAG job yet"},"unprocessedChunks":{"type":"integer","description":"Total chunks across those files"},"estimatedTokens":{"type":"integer","description":"Estimated extraction tokens (includes a 1.2x safety buffer)"}}}}}},"403":{"description":"Not authorized to edit this Digital Twin"},"404":{"description":"Digital Twin not found"},"500":{"description":"Server error"}}}},"/api/user/institution/{id}/kag/backfill-diagnose":{"get":{"summary":"Read-only funnel diagnostic for a Digital Twin's KAG backfill quote","description":"Explains why the backfill quote is what it is by returning the full funnel (counts only, no mutation): total uploads → eligible (status + user) → embedded (RAG-ready) → with a completed graph-v2 job → pending. Also breaks down upload statuses and graph-v2 job statuses so an admin can reconcile the quote against the status endpoint's chunk totals.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Institution (Digital Twin) id"}],"responses":{"200":{"description":"Funnel counts","content":{"application/json":{"schema":{"type":"object","properties":{"extractionVersion":{"type":"string"},"totalUploads":{"type":"integer"},"uploadStatusBreakdown":{"type":"object","additionalProperties":{"type":"integer"}},"eligibleUploads":{"type":"integer"},"eligibleWithEmbeddings":{"type":"integer"},"eligibleWithGraphV2DoneJob":{"type":"integer"},"pendingWithEmbeddings":{"type":"integer","description":"Equals backfill-quote.unprocessedFiles"},"graphV2JobsForInstitution":{"type":"integer"},"graphV2JobStatusBreakdown":{"type":"object","additionalProperties":{"type":"integer"}}}}}}},"403":{"description":"Not authorized to edit this Digital Twin"},"404":{"description":"Digital Twin not found"},"500":{"description":"Server error"}}}},"/api/user/institution/{id}/kag/backfill":{"post":{"summary":"Start (or resume) KAG backfill for a Digital Twin's vault","description":"Enqueues knowledge-graph extraction jobs for the institution's eligible files. Returns 409 if a backfill is already in progress for this Digital Twin. Backfill jobs run at a lower priority than live uploads.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Institution (Digital Twin) id"}],"responses":{"200":{"description":"Backfill enqueued","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"summary":{"type":"object","description":"Per-upload enqueue summary (created / requeued / alreadyDone counts)"}}}}}},"403":{"description":"Not authorized to edit this Digital Twin"},"404":{"description":"Digital Twin not found"},"409":{"description":"A backfill is already in progress for this Digital Twin","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"inFlight":{"type":"integer"}}}}}},"500":{"description":"Server error"}}}},"/api/user/institution/{id}/kag/backfill/status":{"get":{"summary":"Aggregate status of a Digital Twin's KAG jobs","description":"Aggregates the institution's knowledge-graph jobs into a single progress view. `state` is idle when there are no jobs, otherwise running / queued / error / done derived across all jobs.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Institution (Digital Twin) id"}],"responses":{"200":{"description":"Aggregated status","content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string","enum":["idle","queued","running","error","done"]},"processedChunks":{"type":"integer"},"totalChunks":{"type":"integer"},"tokensIn":{"type":"integer"},"tokensOut":{"type":"integer"},"error":{"type":"string","nullable":true}}}}}},"403":{"description":"Not authorized to edit this Digital Twin"},"404":{"description":"Digital Twin not found"},"500":{"description":"Server error"}}}},"/api/user/institution/{id}/kag/backfill/active":{"delete":{"summary":"Cancel a Digital Twin's in-progress KAG backfill","description":"Marks in-flight backfill jobs (lower-priority) as skipped. Live uploads and already-completed graph data are left intact.\n","tags":["User Profile","KAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Institution (Digital Twin) id"}],"responses":{"200":{"description":"Cancellation applied","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"cancelled":{"type":"integer","description":"Number of in-flight backfill jobs cancelled"}}}}}},"403":{"description":"Not authorized to edit this Digital Twin"},"404":{"description":"Digital Twin not found"},"500":{"description":"Server error"}}}},"/api/user/me/mfa-enroll":{"post":{"summary":"Send a verification code to prove email ownership (post-enable enrollment)","description":"Called by the client right after PUT /api/user/me {mfaEnabled:true}. Creates a 5-min challenge, emails the 6-digit code, returns challengeId + maskedEmail so the frontend can launch the verify screen. On success the user gets a trusted-device cookie; on cancel the frontend rolls mfaEnabled back to false to prevent self-lockout.","tags":["User Profile"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Code emailed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"challengeId":{"type":"string"},"maskedEmail":{"type":"string","example":"hu***@praxis-ai.com"}}}}}},"401":{"description":"Authentication required."},"500":{"description":"MFA_NOT_CONFIGURED — MFA_PEPPER env missing on server."},"502":{"description":"Email provider failure — challenge invalidated."}}}},"/api/user/me/mfa-prompt-dismiss":{"post":{"summary":"Dismiss the enable-MFA nudge for 7 days","description":"Records `mfaPromptDismissedAt = now` on the current user. The post-\nlogin MFA opt-in screen will suppress itself for 7 days from this\ntimestamp before reappearing.\n","tags":["User Profile"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Dismissal recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true}}}}}},"401":{"description":"Authentication required."}}}},"/api/user/me/trusted-devices":{"get":{"summary":"List the current user's active MFA-trusted devices","description":"Returns every non-revoked, non-expired `trusted_device` row for the\nauthenticated user, sorted by `lastUsedAt` descending. These rows\nback the 7-day MFA-skip cookie — revoking one forces the matching\nbrowser to re-MFA on its next login.\n","tags":["User Profile"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of trusted devices.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/TrustedDevice"}}}}}}}}},"delete":{"summary":"Revoke ALL trusted devices for the current user","description":"Sets `revokedAt` on every active trusted-device row for the user.\nAlso clears the pria_mfa_trust cookie from this browser. Users will\nsee re-MFA on their next login on every device.\n","tags":["User Profile"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Devices revoked. `revoked` is the count of rows updated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"revoked":{"type":"integer"}}}}}}}}},"/api/user/me/trusted-devices/{deviceId}":{"delete":{"summary":"Revoke a specific trusted device","description":"Targets a single trusted-device row by its `deviceId` (the opaque hex value).","tags":["User Profile"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Device revoked (if it belonged to the current user and was active).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"revoked":{"type":"integer"}}}}}}}}},"/api/user/usage-history":{"get":{"summary":"Per-bucket credits spent for the calling user within an instance scope","description":"Returns time-bucketed credits and message counts for the authenticated user, scoped either to `personal` usage (History rows with no institution) or to a single institution the caller belongs to. The aggregation is DocumentDB-safe (no `$facet`, no `$dateTrunc`, no `.collation()`); buckets are padded with zero-value entries so the chart renders contiguous bars. Results are cached in-memory for 60 seconds per `user × instance × window × interval × scope` (see the `X-Cache` response header). Consumed by the Usage tab in the Pricing pane. When the effective scope is `all`, buckets and totals additionally carry distinct active-user counts.\n","tags":["User"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"query","name":"instanceId","required":true,"schema":{"type":"string"},"description":"Either the literal string `personal` (personal-scope usage) or the ObjectId of an institution the caller is a member of.\n","example":"personal"},{"in":"query","name":"window","required":false,"schema":{"type":"integer","enum":[7,14,30,90,180],"default":30},"description":"Number of days of history to aggregate. Invalid values fall back to 30."},{"in":"query","name":"interval","required":false,"schema":{"type":"string","enum":["daily","weekly","monthly"]},"description":"Bucket granularity. Invalid or omitted values fall back to a window-appropriate default (daily ≤30d, weekly otherwise).\n"},{"in":"query","name":"scope","required":false,"schema":{"type":"string","enum":["own","all"],"default":"own"},"description":"`own` (default) returns the calling user's own usage for the instance. `all` returns aggregated usage for ALL users of the institution — but is honored ONLY when the caller is an admin of that institution (super, or admin with `institutions.edit`). For non-admins, or for `personal` scope, it silently falls back to `own` (never leaks other users' data). The EFFECTIVE scope is echoed in the response `scope` field. The 60s TTL cache key includes the effective scope, so own and all-users payloads never collide.\n"}],"responses":{"200":{"description":"Bucketed usage payload","headers":{"X-Cache":{"schema":{"type":"string","enum":["HIT","MISS"]},"description":"Whether the payload was served from the 60s TTL cache."}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"instanceId":{"type":"string","example":"personal"},"window":{"type":"integer","example":30},"interval":{"type":"string","example":"daily"},"scope":{"type":"string","enum":["own","all"],"description":"The EFFECTIVE scope applied (falls back to `own` if the caller isn't an institution admin).","example":"own"},"currency":{"type":"string","example":"usd"},"buckets":{"type":"array","description":"One entry per granular bucket, ascending by start.","items":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"ISO timestamp of the bucket start (UTC).","example":"2026-05-01T00:00:00.000Z"},"credits":{"type":"integer","description":"Credits spent in the bucket.","example":12},"messages":{"type":"integer","description":"Conversation messages counted in the bucket.","example":4},"users":{"type":"integer","description":"Distinct users active in the bucket. Present ONLY when the effective scope is `all` (institution admin).\n","example":7}}}},"totals":{"type":"object","properties":{"credits":{"type":"integer","example":120},"messages":{"type":"integer","example":48},"users":{"type":"integer","description":"Distinct users active across the whole window (true distinct count, not a sum of bucket values). Present ONLY when the effective scope is `all`.\n","example":47}}}}}}}},"400":{"description":"Missing or invalid instanceId","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"instanceId required"}}}}}},"403":{"description":"Caller is not a member of the requested institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Forbidden"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}}}}},"/api/user/api-key-usage":{"get":{"summary":"List devices that have used the caller's personal API key","description":"Returns the caller's own per-device usage rows for their personal API\nkey, ordered by most-recently-seen, capped at 50. Each successful\nPOST /api/auth/api-key-signin upserts a row keyed by\nsha256(ipSubnet24 | uaFamily | uaOs). Rotating or revoking the key\nwipes all rows (the list resets to empty). Records auto-expire 180\ndays after lastSeenAt.\n","tags":["User Profile"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List returned (may be empty).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyUsageDevice"}}}}}}},"401":{"description":"Authentication required."},"500":{"description":"Server error."}}}},"/api/user/api-key-usage/{id}":{"delete":{"summary":"Forget a device row from the caller's list (cosmetic only)","description":"Deletes one of the caller's own apiKeyUsage rows so it disappears\nfrom their list. This does NOT invalidate access — anyone who still\nholds the API key can sign in again and a new row will be recorded.\nTo actually revoke access, rotate the key via POST /api/user/api-key\n(which wipes ALL device rows in one go). Returns 404 for both \"row\nnot found\" and \"row belongs to another user\" to avoid leaking the\nexistence of other users' rows.\n","tags":["User Profile"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Mongo ObjectId of the row to forget."}],"responses":{"200":{"description":"Row forgotten.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Authentication required."},"404":{"description":"Row not found (or not owned by the caller — collapsed for privacy)."},"500":{"description":"Server error."}}}},"/api/user/assistants":{"post":{"summary":"Search and retrieve assistants","description":"Retrieves a filtered list of assistants based on type, name, and the requesting user's permissions. Results include system assistants (user=null), the user's own assistants, institution-shared assistants, and account-shared assistants from sibling institutions. Assistants with status \"deleted\" are always excluded. Unpublished assistants from other users are excluded. Non-shared assistants from other users are excluded for regular users. Assistants disabled by the user's institution are excluded. For non-super users viewing system assistants, the instructions field is omitted. Fields argument_1 through argument_4 are always stripped from each result. Response includes denormalized user_data, institution_data, and account_data objects for display purposes.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["admin","user","instance","account"],"description":"Type filter for assistants. \"admin\" returns system assistants (user=null, institution=null). \"user\" returns the requesting user's own non-shared assistants. \"instance\" returns institution-shared but not account-shared assistants. \"account\" returns assistants shared across the entire account. When omitted, returns all visible assistants using an $or query across categories.\n"},"name":{"type":"string","description":"Regex search filter applied to both name and description fields (case-insensitive)"},"limit":{"type":"integer","description":"Maximum number of assistants to return per query","default":100},"lean":{"type":"boolean","description":"When true, omits the instructions field from all results","default":false},"admin_only":{"type":"boolean","description":"When true and the requesting user is admin/super, filters to admin-only assistants. Regular users always have admin_only assistants excluded automatically."}}},"examples":{"allAssistants":{"summary":"Retrieve all visible assistants","value":{}},"searchByType":{"summary":"Search system assistants by name","value":{"type":"admin","name":"cod"}},"leanUserAssistants":{"summary":"Lean user assistants (no instructions)","value":{"type":"user","lean":true,"limit":50}}}}}},"responses":{"200":{"description":"Successfully retrieved assistants list","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/Assistant"}}}}}}},"400":{"description":"Bad request or server error during query","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object"},"message":{"type":"string","example":"Error getting assistants <details>"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}}}},"/api/user/assistant":{"post":{"summary":"Create a new assistant","description":"Creates a new AI assistant with the specified configuration. The request body is passed directly to the Mongoose model constructor. Only the name field is required by the schema (other fields have defaults). If institution is an empty string it is removed from the body. Auto-reset logic is applied before saving. If institution_shared is true but no institution is provided, institution_shared is reset to false. If account_shared is true but institution_shared is not true, account_shared is reset to false. Duplicate assistants (same name + user + institution) are caught as a 400 error by the unique index constraint, not as a 409.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Name of the assistant (required, must be unique per user+institution)","example":"New Assistant 1"},"status":{"type":"string","enum":["active","unpublished","inactive","deleted"],"description":"Status of the assistant","default":"active","example":"active"},"description":{"type":"string","description":"Description of the assistant","default":"","example":"New Assistant Description"},"instructions":{"type":"string","description":"Behavioral instructions for the assistant","default":"","example":"New Assistant Instructions"},"argument_1":{"type":"string","nullable":true,"description":"Optional argument 1"},"argument_2":{"type":"string","nullable":true,"description":"Optional argument 2"},"argument_3":{"type":"string","nullable":true,"description":"Optional argument 3"},"argument_4":{"type":"string","nullable":true,"description":"Optional argument 4"},"argument_5":{"type":"string","nullable":true,"description":"Optional argument 5"},"picture_url":{"type":"string","description":"URL for the assistant's profile picture","example":"https://example.com/image.jpeg"},"admin_only":{"type":"boolean","description":"Whether the assistant is restricted to admin users","default":false,"example":true},"remember_history":{"type":"integer","description":"Number of conversation history items to remember","default":3,"example":3},"editable_others":{"type":"boolean","description":"Whether the assistant can be edited by other users","default":false,"example":true},"user":{"type":"string","description":"User ObjectId who owns this assistant","example":"6430d02554cd4e00403e8b05"},"institution":{"type":"string","description":"Institution ObjectId. If empty string, field is removed.","example":"6631915765bb0a94cfd6ca99"},"institution_shared":{"type":"boolean","description":"Whether shared with the institution. Auto-reset to false if no institution provided.","default":false,"example":true},"account_shared":{"type":"boolean","description":"Whether shared across the account. Auto-reset to false if institution_shared is not true.","default":false,"example":false},"ragEnabled":{"type":"boolean","description":"Whether RAG is enabled for this assistant","default":true},"bypassSystemContext":{"type":"boolean","description":"Whether to bypass the system context prompt","default":false},"conversationModel":{"type":"string","description":"Optional model override for this assistant (max 200 chars)"},"stsConversationModel":{"type":"string","description":"Optional STS/voice conversation model; used only in ElevenLabs/Anam/LemonSlice voice mode (max 200 chars)"}}}}}},"responses":{"201":{"description":"Assistant successfully created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"id":{"type":"string","description":"ObjectId of the created assistant","example":"687e761093c797174459c651"},"message":{"type":"string","example":"Assistant created!"}}}}}},"400":{"description":"Bad request - empty body, invalid properties, or duplicate name (unique index violation)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object","description":"The raw error object"},"message":{"type":"string","example":"Error creating assistant <details>"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}}}},"/api/user/assistant/{id}":{"get":{"summary":"Get a single assistant by ID","description":"Retrieves a single assistant by its ObjectId. Accessible to (a) the assistant owner, (b) admin users with the relevant entitlement on the assistant's institution, or (c) super users. Standard users who don't own the assistant get 403. The response includes populated user_data (email, fname, lname, institution, accountType), institution_data (name, ainame, status, picture), and account_data (name, status) objects. For non-super users viewing system assistants (user=null), the instructions field is omitted.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to retrieve","example":"687e761093c797174459c651"}],"responses":{"200":{"description":"Successfully retrieved assistant","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"$ref":"#/components/schemas/Assistant"}}}}}},"400":{"description":"Invalid assistant ID or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object"},"message":{"type":"string","example":"Invalid Assistant Id"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - caller is not the owner and does not have admin/super access for this assistant's institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not authorized to view this assistant"}}}}}},"404":{"description":"Assistant not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Assistant not found"}}}}}}}},"put":{"summary":"Update an existing assistant","description":"Updates an existing assistant by its ObjectId. Accepts any non-empty object as the request body and passes it directly to MongoDB updateOne. No field-level validation is performed beyond requiring a non-empty body. Special handling for institution and user fields when sent as empty strings uses $unset to remove them from the document. Auto-reset logic is applied before update. If account_shared is true but institution_shared is not true, account_shared is reset to false. Any authenticated user can call this endpoint (no admin check).\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to update","example":"687e761093c797174459c651"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Any valid assistant fields. No required fields for update. Empty strings for user/institution trigger $unset.","properties":{"name":{"type":"string","description":"Name of the assistant","example":"New Assistant Updated"},"status":{"type":"string","enum":["active","unpublished","inactive","deleted"],"description":"Status of the assistant","example":"active"},"description":{"type":"string","description":"Description of the assistant","example":"New Assistant Description"},"instructions":{"type":"string","description":"Instructions for the assistant","example":"New Assistant Instructions"},"argument_1":{"type":"string","nullable":true},"argument_2":{"type":"string","nullable":true},"argument_3":{"type":"string","nullable":true},"argument_4":{"type":"string","nullable":true},"argument_5":{"type":"string","nullable":true},"picture_url":{"type":"string","description":"URL for the assistant's profile picture"},"admin_only":{"type":"boolean","description":"Whether the assistant is admin-only"},"remember_history":{"type":"integer","description":"Number of conversation history items to remember"},"editable_others":{"type":"boolean","description":"Whether the assistant can be edited by others"},"user":{"type":"string","description":"User ObjectId. Empty string triggers $unset to remove the field.","example":"6430d02554cd4e00403e8b05"},"institution":{"type":"string","description":"Institution ObjectId. Empty string triggers $unset to remove the field.","example":"6631915765bb0a94cfd6ca99"},"institution_shared":{"type":"boolean","description":"Whether shared with the institution"},"account_shared":{"type":"boolean","description":"Whether shared across the account. Auto-reset to false if institution_shared is not true."},"ragEnabled":{"type":"boolean","description":"Whether RAG is enabled"},"bypassSystemContext":{"type":"boolean","description":"Whether to bypass the system context prompt"},"conversationModel":{"type":"string","description":"Optional model override (max 200 chars)"},"stsConversationModel":{"type":"string","description":"Optional STS/voice conversation model; used only in ElevenLabs/Anam/LemonSlice voice mode (max 200 chars)"}}}}}},"responses":{"200":{"description":"Assistant successfully updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Assistant updated!"}}}}}},"400":{"description":"Bad request - invalid ID, empty body, or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object"},"message":{"type":"string","example":"Invalid Properties"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"404":{"description":"Assistant not found (matchedCount is 0)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Assistant not found!"}}}}}}}},"delete":{"summary":"Soft-delete an assistant","description":"Performs a soft delete on an assistant by setting its status to \"deleted\" and prepending a timestamp to the name (to free the unique index slot). Requires admin or super user privileges. System assistants (user=null) cannot be deleted through this endpoint and return a 403 error. Admin users can only delete assistants they own; attempting to delete another admin's assistant returns a 403. Super users can delete any non-system assistant.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to delete","example":"687e761093c797174459c651"}],"responses":{"200":{"description":"Assistant successfully soft-deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Assistant Deleted"}}}}}},"400":{"description":"Bad request - invalid ID or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object"},"message":{"type":"string","example":"Error deleting assistant <details>"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - not admin/super, system assistant, or another admin's assistant","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"Possible values: 'Assistants can only be deleted by admin and super admins', 'A system assistant must be deleted from the admin console!', 'Not authorized to delete assistant from another admin!'","example":"Assistants can only be deleted by admin and super admins"}}}}}},"404":{"description":"Assistant not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Assistant not found"}}}}}}}}},"/api/user/assistantLikesCount/{id}":{"put":{"summary":"Increment an assistant's liked count","description":"Increments the liked_count field of an assistant using MongoDB $inc operator. The liked_count value from the request body is added to the current count (can be negative to decrement). Any authenticated user can call this endpoint.\n","tags":["Assistants"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the assistant to update","example":"687e761093c797174459c651"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["liked_count"],"properties":{"liked_count":{"type":"integer","description":"Value to increment the liked_count by (use negative to decrement). Uses MongoDB $inc operator.","example":1}}}}}},"responses":{"200":{"description":"Assistant liked count successfully updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Assistant updated!"}}}}}},"400":{"description":"Bad request - invalid ID, empty body, missing liked_count, or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Assistant property required, ex: liked_count"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"404":{"description":"Assistant not found (matchedCount is 0)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Assistant not found!"}}}}}}}}},"/api/user/audio-notes":{"post":{"tags":["Audio Notes"],"summary":"Upload one or more audio segments for transcription and vault ingestion","description":"Accepts 1..20 audio segments (audio/webm, audio/ogg, audio/mp4, audio/mpeg, audio/wav).\nEach segment is capped at 50 MB. When `combine=true` and all segments share a single\nmimetype, the server concatenates them losslessly via ffmpeg before ingestion.\nReturns 202 immediately; transcription runs asynchronously through the existing\ningestion queue and produces a transcript-derived file_title.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["files"],"properties":{"files":{"type":"array","items":{"type":"string","format":"binary"}},"combine":{"type":"string","enum":["true","false"],"description":"When true and all segments share a single mimetype, the server losslessly concatenates them via ffmpeg into a single upload before ingestion. Falls back to per-segment uploads on mismatch or concat failure."},"segment_names":{"type":"array","items":{"type":"string"},"description":"Optional names parallel to files[]; ignored when combine=true"},"selectedCourse":{"type":"string","description":"Optional JSON-stringified course context (`{ course_id, course_name, assistant: { _id } }`) — applied to the History row that runFinalize updates as ingestion completes. Same shape as `POST /user/files`.\n"},"selectedAssistant":{"type":"string","description":"Optional active-assistant ObjectId — used as the History row's `assistant` field when no `selectedCourse.assistant` is provided."}}}}}},"responses":{"202":{"description":"Accepted — uploads queued for background ingestion","content":{"application/json":{"schema":{"type":"object","properties":{"acknowledged":{"type":"boolean"},"uploads":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"originalname":{"type":"string"},"status":{"type":"string"}}}}}}}}},"400":{"description":"Invalid input (no files, file too large, etc.)"},"403":{"description":"Personal upload disabled by institution"}}}},"/api/user/collections":{"post":{"summary":"List collections for a vault","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"vault":{"type":"string","enum":["personal","instance","account"]},"page":{"type":"integer"},"pageSize":{"type":"integer"},"institution":{"type":"string"},"parent":{"type":"string","description":"Filter by parent collection ID (omit for root collections)"},"fileNameSearch":{"type":"string","description":"Hide collections whose name does not match AND whose descendants contain no matching files (case-insensitive substring)"},"nameOrder":{"type":"boolean","description":"When true, sorts by created date instead of the default case-insensitive name sort"},"sortAscending":{"type":"boolean","description":"When true, sorts in ascending order; otherwise descending"}}}}}},"responses":{"200":{"description":"Paginated list of collections with stats"}}}},"/api/user/collection":{"post":{"summary":"Create a new collection","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"institution":{"type":"string"},"account_shared":{"type":"boolean"},"parent":{"type":"string","description":"Parent collection ID (optional, creates sub-collection)"},"color":{"type":"string","nullable":true,"pattern":"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$","description":"Optional accent color (`#RGB` / `#RRGGBB`) applied to the folder icon.","example":"#10b981"}}}}}},"responses":{"201":{"description":"Collection created"}}}},"/api/user/collection/{id}":{"put":{"summary":"Update collection (rename, move vault)","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"institution":{"type":"string"},"account_shared":{"type":"boolean"},"parent":{"type":"string","nullable":true,"description":"New parent collection ID (null to move to root)"},"color":{"type":"string","nullable":true,"pattern":"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$","description":"Accent color (`#RGB` / `#RRGGBB`). Pass `null` or `\"\"` to clear and revert to the default outline icon."}}}}}},"responses":{"200":{"description":"Collection updated"},"403":{"description":"Forbidden - standard user does not own the collection; standard user attempted to move a collection to an instance or account vault; or admin lacks files.edit entitlement on the collection's institution"},"404":{"description":"Collection not found"}}},"delete":{"summary":"Delete collection and all contained files","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Collection and files deleted"}}}},"/api/user/collection/{id}/files":{"post":{"summary":"List files within a collection","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lean":{"type":"boolean"},"compact":{"type":"boolean"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"fileNameSearch":{"type":"string"},"status":{"type":"string","enum":["inactive","selected","active","error","deleted","excluded","processing"],"description":"Filter the collection's files by status. Mirrors the shape used by `/api/user/uploads`:\n  - Real DB values (`inactive`/`selected`/`active`/`error`/`deleted`) match exactly.\n  - Pseudo-value `excluded` expands to `status $nin:['selected','deleted']`.\n  - Pseudo-value `processing` matches files currently mid-ingestion (`ingestion.phase ∈ {extract, chunk, sanitize, embed, kag}` AND `status $ne:'deleted'`).\nWhen omitted, defaults to $ne:'deleted'.\n"},"nameOrder":{"type":"boolean"},"sortAscending":{"type":"boolean"},"sortBy":{"type":"string"}}}}}},"responses":{"200":{"description":"Paginated list of files in the collection, plus child sub-collections","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","description":"Array of UploadFile objects. In lean/compact mode, each file is enriched with owner_data (for instance/account files), institution_name, and institution_display_name (for account_shared files).","items":{"$ref":"#/components/schemas/UploadFile"}},"childCollections":{"type":"array","description":"Direct child sub-collections with stats","items":{"$ref":"#/components/schemas/Collection"}},"total":{"type":"integer"},"hasMore":{"type":"boolean"},"page":{"type":"integer"},"pageSize":{"type":"integer"}}}}}}}}},"/api/user/collection/{id}/add":{"put":{"summary":"Add files to a collection","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Upload IDs to add to the collection"}}}}}},"responses":{"200":{"description":"Files added to collection"}}}},"/api/user/collection/{id}/remove":{"put":{"summary":"Remove files from a collection","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Upload IDs to remove from the collection"}}}}}},"responses":{"200":{"description":"Files removed from collection"}}}},"/api/user/collection/{id}/download":{"get":{"summary":"Download collection as ZIP","tags":["Collections"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ZIP file stream","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/user/bulk/action":{"post":{"summary":"Bulk action on files or collections","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids","type","action"],"properties":{"ids":{"type":"array","items":{"type":"string"}},"type":{"type":"string","enum":["upload","collection"]},"action":{"type":"string","enum":["delete","include","exclude","reprocess","moveToCollection","makePublic","makePrivate","makeConfidential","removeConfidential"]},"target":{"type":"string","description":"Target collection ID for moveToCollection action"}}}}}},"responses":{"200":{"description":"Bulk action completed"}}}},"/api/user/collections/select-all-ids":{"post":{"summary":"Get all file and collection IDs for bulk select-all","description":"Returns all file IDs and collection IDs matching the current scope (vault\nroot or inside a collection), honoring the active filters from the UI\n(file-name search, status filter). When any filter is active, collections\nare NOT returned — selecting a whole collection would pull in non-matching\nfiles inside it, which is unsafe for bulk delete.\n","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"vault":{"type":"string","enum":["personal","instance","account"],"description":"Vault scope (required at root level)"},"parent":{"type":"string","nullable":true,"description":"Collection ID to scope within (null for vault root)"},"institution":{"type":"string","description":"Override institution ID"},"fileNameSearch":{"type":"string","description":"Case-insensitive substring filter on file originalname"},"status":{"type":"string","description":"Filter files by status. 'excluded' means $nin [selected, deleted].","enum":["active","inactive","selected","error","deleted","excluded"]}}}}}},"responses":{"200":{"description":"Selection IDs and names","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"files":{"type":"array","description":"Files that will be selected (id + name preview)","items":{"type":"object","properties":{"_id":{"type":"string"},"originalname":{"type":"string"}}}},"collections":{"type":"array","description":"Collections that will be selected (empty when a filter is active)","items":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"fileCount":{"type":"integer"}}}},"fileIds":{"type":"array","description":"Legacy bare-id list (mirrors files[]._id)","items":{"type":"string"}},"collectionIds":{"type":"array","description":"Legacy bare-id list (mirrors collections[]._id)","items":{"type":"string"}},"totalFiles":{"type":"integer"},"filtered":{"type":"boolean","description":"True when a fileNameSearch or status filter scoped the result"}}}}}}}}},"/api/user/collections/deep-count":{"post":{"summary":"Get recursive file counts for collections","description":"Given an array of collection IDs, returns the real recursive file count for each (including files in all descendant sub-collections). Does not use cached fileCount.","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Collection IDs (max 100)"}}}}}},"responses":{"200":{"description":"Deep counts per collection","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"counts":{"type":"object","additionalProperties":{"type":"integer"},"description":"Map of collection ID to recursive file count"}}}}}}}}},"/api/user/bulk/download":{"post":{"summary":"Download selected files as ZIP","description":"Streams a ZIP archive of the selected files. When only one file is selected, streams the file directly without zipping. A 1GB total size limit is enforced. Accepts both upload IDs and collection IDs — files in collections are included. Duplicate files are deduplicated by ID. ZIP entry names are deduplicated with a counter suffix.\n","tags":["Collections"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"uploadIds":{"type":"array","items":{"type":"string"},"description":"Upload IDs to include"},"collectionIds":{"type":"array","items":{"type":"string"},"description":"Collection IDs whose files should be included"},"names":{"type":"array","items":{"type":"string"},"description":"Display names used to build the ZIP filename"},"ids":{"type":"array","items":{"type":"string"},"description":"Legacy alias for uploadIds (backward compat)"}}}}}},"responses":{"200":{"description":"ZIP file stream (or single file stream when only one file)","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"No downloadable files found or missing IDs"},"413":{"description":"Selection exceeds 1GB size limit"}}}},"/api/user/embeddings":{"post":{"summary":"Get embedding chunks for an upload","description":"Retrieves all embedding chunks for a specific IP Vault upload, sorted by chunk index.\nEach uploaded file is split into text chunks and converted into vector embeddings for\nretrieval-augmented generation (RAG). This endpoint returns the chunk metadata and text\ncontent (up to 1000 chunks per request).\n","tags":["RAG"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEmbeddingsRequest"}}}},"responses":{"200":{"description":"Embedding chunks retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetEmbeddingsResponse"}}}},"400":{"description":"Bad request - Missing or invalid upload ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/embedding":{"post":{"summary":"Create a new embedding chunk","description":"Creates a new embedding chunk for an existing upload. The chunk text is vectorized\nautomatically using the institution's configured embedding model. The new chunk is\nappended after the last existing chunk (highest chunkIndex + 1).\n\nUse this to manually extend a file's RAG segments with additional text content\nthat wasn't captured during automatic ingestion.\n","tags":["RAG"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmbeddingRequest"}}}},"responses":{"201":{"description":"Embedding chunk created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmbeddingResponse"}}}},"400":{"description":"Bad request - Missing upload ID or chunkText","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Upload not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/embedding/{id}":{"put":{"summary":"Update an embedding chunk","description":"Updates an existing embedding chunk. When `chunkText` is modified, the vector\nembedding is automatically regenerated to keep the semantic search index in sync\nwith the text content.\n","tags":["RAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Embedding chunk ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmbeddingRequest"}}}},"responses":{"200":{"description":"Embedding chunk updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmbeddingResponse"}}}},"400":{"description":"Bad request - Invalid embedding ID or empty body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Embedding chunk not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"summary":"Delete an embedding chunk","description":"Permanently deletes a single embedding chunk by ID.","tags":["RAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Embedding chunk ID"}],"responses":{"200":{"description":"Embedding chunk deleted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteEmbeddingResponse"}}}},"400":{"description":"Bad request - Invalid embedding ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/embedding/{id}/sanitize":{"post":{"summary":"Sanitize an embedding chunk with AI","description":"Sends the chunk text to the institution's summary model for AI-powered cleanup.\nRemoves noise (navigation, boilerplate, encoding artifacts), normalizes whitespace,\nand fixes broken formatting — without summarizing or shortening the content.\n\nThe sanitized text is returned for preview but **not persisted**. To save the\ncleaned text, call `PUT /api/user/embedding/{id}` with the returned `sanitizedText`.\n\nToken usage is automatically tallied to the parent Upload's `tokens_used` counter.\n","tags":["RAG"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Embedding chunk ID to sanitize"}],"responses":{"200":{"description":"Segment sanitized successfully (preview only — not saved)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SanitizeEmbeddingResponse"}}}},"400":{"description":"Bad request - Invalid ID or empty segment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Embedding chunk not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Sanitization produced empty result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/feedback":{"post":{"summary":"Submit feedback","description":"Submits user feedback about the application. Sends a notification email upon successful creation.","tags":["Feedback"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackRequest"}}}},"responses":{"200":{"description":"Feedback submitted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackCreateSuccess"}}}},"400":{"description":"Bad request - invalid user, missing body, missing feedback text, or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"Error detail (present on validation and server errors)"},"message":{"type":"string","description":"Error message (present on invalid user and server errors)"}}}}}}}}},"/api/user/feedbacks":{"get":{"summary":"Get user feedbacks","description":"Retrieves feedback submitted by the current user. Returns up to 100 items sorted by newest first. Response is a raw JSON array (no wrapper object).","tags":["Feedback"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Array of feedback documents","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Feedback"}}}}},"400":{"description":"Server error retrieving feedbacks","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"Error detail"},"message":{"type":"string","description":"Error message"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}}}}},"/api/user/url":{"post":{"summary":"Upload content from URL","description":"Downloads or scrapes content from a URL and ingests it into the user's IP Vault via RAG. Google document URLs (Docs, Sheets, Slides) are automatically detected and read via Google OAuth instead of plain HTTP download. The URL is validated against SSRF in non-dev mode. When an institution is provided, the user's membership is verified before proceeding.","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlRequest"}}}},"responses":{"200":{"description":"URL content ingested successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlResponse"}}}},"400":{"description":"Bad request - invalid URL, SSRF violation, unauthorized institution, or RAG processing error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid url"},"error":{"description":"Error object from the failed operation"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - multiple causes: (a) Google OAuth authorization required - the URL is a Google document but the user has no valid Google OAuth token; (b) standard user attempted to upload to an instance vault; (c) standard user personal upload denied because the institution has disableFileUploadForUser=true and the user's email domain is not in enableFileUploadForEmail; (d) admin lacks files.add entitlement on the target institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"code":{"type":"string","enum":["GOOGLE_AUTH_REQUIRED","GOOGLE_AUTH_ERROR","SERVICES_DISABLED_BY_ADMIN","TOKEN_REFRESH_FAILED","SERVICE_NOT_ENABLED","TOKEN_MISSING"],"description":"Specific error code from the Google OAuth cascade (only present for Google OAuth 403 scenarios)"},"message":{"type":"string","example":"Google authorization is required to read this document."},"consentUrl":{"type":"string","nullable":true,"description":"OAuth consent URL if available (user can authorize directly, Google OAuth case only)","example":"https://pria.praxislxp.com/api/auth/google/services/authorize?scopes=drive,document&token=..."}}}}}}}}},"/api/user/files":{"post":{"summary":"Upload files","description":"Uploads one or more files to the user's IP Vault for RAG processing and embedding. Files are uploaded via multipart form-data using the 'files' field name. Audio files (mp3, m4a, webm, wav, mp4, mov, etc.) are limited to 25MB each. Non-audio files are limited to 150MB each. The multer middleware enforces a 500MB overall limit per file before the handler's own size checks.","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["files"],"properties":{"files":{"type":"array","items":{"type":"string","format":"binary"},"description":"File(s) to upload. Audio files limited to 25MB, non-audio limited to 150MB."},"confidential":{"type":"string","description":"Legacy global confidential flag (string 'true'/'false'). Only applied when institution is provided. Overridden per-file by fileConfidential.","example":"false"},"fileConfidential":{"type":"string","description":"JSON string mapping original filenames to per-file confidential booleans. Example: '{\"report.pdf\": true, \"notes.txt\": false}'. Overrides the global confidential flag.","example":"{\"report.pdf\": true, \"notes.txt\": false}"},"skipIndexing":{"type":"string","description":"Global skip-indexing flag (string 'true'/'false'). When true, all files in this upload batch skip RAG embedding generation and are set to status 'active' with no embeddings. Overridden per-file by fileSkipIndexing.","example":"false"},"fileSkipIndexing":{"type":"string","description":"JSON string mapping original filenames to per-file skip-indexing booleans. Example: '{\"report.pdf\": true, \"notes.txt\": false}'. Overrides the global skipIndexing flag.","example":"{\"report.pdf\": true}"},"institution":{"type":"string","description":"Institution ID to associate the uploads with","example":"6631915765bb0a94cfd6ca99"},"account_shared":{"type":"string","description":"When 'true' and institution is set, marks files as shared across the institution's account","example":"false"},"selectedCourse":{"type":"string","description":"JSON string with course context for the upload history record. Example: '{\"course_id\": 1750532703472, \"course_name\": \"Research\"}'","example":"{\"course_id\": 1750532703472, \"course_name\": \"Research\"}"},"selectedAssistant":{"type":"string","description":"Assistant ID for the upload history record (used as fallback if selectedCourse.assistant._id is not set)","example":"6856fa89cbafcff8d98680f5"},"collection":{"type":"string","description":"Collection ID to associate the uploaded files with. When provided, each Upload record is linked to this collection.","example":"6631915765bb0a94cfd6ca99"}}}}}},"responses":{"200":{"description":"Upload completed. Individual file results are in the files array; each file has its own success/message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadFilesResponse"}}}},"400":{"description":"Bad request - multer error (file too large, too many files, unexpected field), missing files, invalid user, or processing error","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/MulterErrorResponse"},{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to upload files"},"error":{"description":"Error object from the failed operation"}}}]}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - standard user attempted to upload to an instance vault; standard user personal upload denied because the institution has disableFileUploadForUser=true and the user's email domain is not in enableFileUploadForEmail; or admin lacks files.add entitlement on the target institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"File uploads are disabled for your account"}}}}}}}}},"/api/user/files/{fileId}":{"delete":{"summary":"Delete uploaded file","description":"Deletes a file and its associated embeddings from the user's IP Vault via RAG","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string"},"description":"The file ID to delete","example":"68566d7c4ec8e0cb02907997"}],"responses":{"200":{"description":"File deleted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteFileResponse"}}}},"400":{"description":"Bad request - invalid user, missing file ID, file not found, or deletion failed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"Error message (e.g. 'Invalid user', 'Invalid File', 'Incorrect or missing file', 'Failed to delete file')"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/files/{fileId}/confidential":{"patch":{"summary":"Update file confidential status","description":"Updates the confidential (is_private) flag on a file. The caller must either own the file or be an institution admin for the file's institution.","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string"},"description":"The file ID to update","example":"68566d7c4ec8e0cb02907997"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileConfidentialRequest"}}}},"responses":{"200":{"description":"File confidential status updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileConfidentialResponse"}}}},"400":{"description":"Bad request - Invalid user or missing file ID","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Invalid user' or 'Invalid file ID'"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - User does not own the file and is not an institution admin","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Unauthorized to modify this file"}}}}}},"404":{"description":"File not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"File not found"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to update file"}}}}}}}}},"/api/user/reload/{fileId}":{"put":{"summary":"Reload file content","description":"Re-processes an existing file through the RAG pipeline, regenerating its embeddings and metadata. On success, returns the refreshed Upload record. On RAG callback error, returns HTTP 200 with success: false.","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string"},"description":"The file ID to reload","example":"68566d7c4ec8e0cb02907997"}],"responses":{"200":{"description":"File reload result. Check the success field - async RAG errors also return HTTP 200 with success: false.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ReloadFileResponse"},{"type":"object","description":"RAG callback error (still HTTP 200)","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error reloading file connection timeout"},"error":{"description":"Error object from the RAG processing failure"},"data":{"$ref":"#/components/schemas/UploadFile"}}}]}}}},"400":{"description":"Bad request - invalid user, missing file ID, or file not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Invalid user', 'Invalid File Id', 'Incorrect or missing file', or 'Failed to reload file ...'"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/reingest/{fileId}":{"put":{"summary":"Re-ingest file from source URL","description":"Re-downloads the file content from its original `source_url`, replaces the file on disk,\nand re-runs the RAG ingestion pipeline — regenerating embeddings, summary, and metadata.\nThe existing Upload record is updated in-place (no new record is created).\n\nOnly works for files that were originally uploaded via URL (`source_url` must be set).\nCollection membership is preserved through the re-ingestion process.\n\n**YouTube URLs** are automatically detected and handled specially: metadata is fetched via\noEmbed, transcripts are extracted with timestamps, and the upload thumbnail and `embed_url`\nare updated. If no captions are available and `skipIndexing` is false, the request fails.\n\nOn failure, the upload's original status is restored to prevent file cards from getting\nstuck in a \"processing\" state.\n","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string"},"description":"The file ID to re-ingest","example":"68566d7c4ec8e0cb02907997"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"skipIndexing":{"type":"boolean","description":"When true, re-downloads the content but skips RAG embedding generation. The file is set to status 'active' with no new embeddings. For YouTube URLs without captions, this allows creating a metadata-only file instead of failing.","default":false}}}}}},"responses":{"200":{"description":"Re-ingest result. Check the success field - async RAG errors also return HTTP 200 with success: false.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Successful re-ingestion","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"File re-ingested from URL"},"data":{"$ref":"#/components/schemas/UploadFile"}}},{"type":"object","description":"RAG callback error (still HTTP 200)","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Re-ingest failed: connection timeout"},"error":{"description":"Error object from the RAG processing failure"}}}]}}}},"400":{"description":"Bad request - invalid user, missing file ID, file not found, or file has no source URL","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Invalid user', 'Invalid File Id', 'File not found', 'File has no source URL', or 'Failed to re-ingest: ...'"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/files/{fileId}/reprocess":{"post":{"summary":"Re-queue an existing file for ingestion","description":"Re-enqueues an existing Upload record through the asynchronous ingestion pipeline\n(extract → chunk → sanitize → embed → finalize) without re-downloading the source file.\nUnlike `/reingest/{fileId}` which re-fetches the source URL, this endpoint operates\non the file already on disk.\n\nReturns 409 if the file is already being processed (phase not in `done`/`error`).\n","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string"},"description":"The file ID to re-queue","example":"68566d7c4ec8e0cb02907997"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["full","embed"],"default":"full","description":"`full` wipes all chunks/embeddings and restarts from the `queued` phase.\n`embed` keeps existing chunks but clears the `embedded` flag + provider-specific vector fields\nand resumes at the `embed` phase — useful after changing the embedding model.\n"}}}}}},"responses":{"202":{"description":"File has been queued for reprocessing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReprocessFileResponse"}}}},"400":{"description":"Bad request - invalid user, file ID, or mode value","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - user does not own the file or lacks files.edit entitlement on the institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}},"404":{"description":"File not found"},"409":{"description":"File is already being processed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"File is already being processed (phase: extract)"},"ingestion":{"type":"object"}}}}}},"503":{"description":"Ingestion queue is not enabled or is full","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Ingestion queue is not enabled"}}}}}}}}},"/api/user/files/{uploadId}/kag-summary":{"get":{"summary":"Get KAG (knowledge graph) summary for a file","description":"Returns the latest knowledge_graph_job for the upload at the current\n`graph-v2` extractionVersion: status, entities/relationships extracted,\nsegments processed, attempt count, and the latest error (if any).\n\nLazy-loaded by the File Preview > Knowledge Graph (KAG) sidebar\nsection so the lean upload list doesn't have to ship this for every\nrow. The endpoint is read-only and per-upload — ownership is enforced\nby the existing vault scope on the upload list (callers can only\nrequest summaries for uploads they can see).\n\nSoft-200 with `{ notReady: true }` when the embeddings Atlas\nconnection is still opening — same convention as the embeddings\nendpoint. Returns `{ exists: false }` when no KAG job has been\nenqueued yet (e.g. file still in chunk/sanitize before KAG\nenqueues).\n","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"uploadId","required":true,"schema":{"type":"string"},"description":"The upload ID","example":"6a145d0d269fc0a663eea267"}],"responses":{"200":{"description":"KAG summary (may be `{exists:false}` or `{notReady:true}`)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"exists":{"type":"boolean","description":"false when no knowledge_graph_job has been enqueued yet"},"notReady":{"type":"boolean","description":"true when the embeddings DB connection is still opening; client should re-poll"},"job":{"type":"object","description":"Only present when exists=true","properties":{"status":{"type":"string","enum":["queued","running","done","error","skipped"]},"progress":{"type":"object","properties":{"percent":{"type":"number"},"message":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}}},"chunksProcessed":{"type":"integer","example":1},"entitiesExtracted":{"type":"integer","example":5},"relationshipsExtracted":{"type":"integer","example":4},"recordsSeen":{"type":"integer"},"recordsValid":{"type":"integer"},"recordsInvalid":{"type":"integer"},"attempts":{"type":"integer"},"error":{"type":"string","description":"Latest error message (empty string when status != error)"},"nextAttemptAt":{"type":"string","format":"date-time","nullable":true},"created":{"type":"string","format":"date-time","description":"Insertion timestamp — when kagEnqueue created the job doc"},"updated":{"type":"string","format":"date-time","description":"Last write timestamp. For status='done' jobs this is effectively the completion stamp (no dedicated completedAt on the schema)."},"startedAt":{"type":"string","format":"date-time","nullable":true,"description":"First-lease timestamp. Set ONCE by kagJobLease on the queued→running transition (via $ifNull so re-leases preserve it). Distinct from `created` (job insertion) and `updated` (last write). Null on legacy rows that predate the field."},"llmProvider":{"type":"string","description":"Catalog group / routing target the job was dispatched through (e.g. 'openrouter', 'inferx'). Stamped by kagInlineProcessor at resolve time so re-runs after a runtime config change show which provider each lease used. Empty string for legacy rows.","example":"openrouter"},"tokensIn":{"type":"integer","description":"Total prompt tokens spent by the graph extractor across all chunks/subchunks/sliding-window batches. Summed via writeJobProgress.","example":1433},"tokensOut":{"type":"integer","description":"Total completion tokens emitted by the extractor across all chunks. Summed via writeJobProgress.","example":571}}}}}}}},"400":{"description":"Invalid upload ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/upload/{fileId}/swap-asset":{"put":{"summary":"Swap (replace) the on-disk asset for an existing Upload","description":"Replaces the physical bytes under an existing Upload record without\nregenerating embeddings or touching ingestion state. The Upload `_id`,\n`filename`, and `file_url` stay byte-identical so any published links\ncontinue to resolve. Existing embeddings, `file_title`, `file_summary`,\nand `vaultHealthScore` are preserved — to re-index the new content,\ncall the Reprocess Content endpoint afterwards.\n\nTwo input modes (mutually exclusive):\n  - Multipart `file` field with the new asset bytes.\n  - JSON `{ url }` body — server fetches the URL and writes it into\n    place. SSRF guard mirrors `/api/user/url`.\n\nAuthorization mirrors the rename endpoint: the caller must own the\nupload, or hold `files.edit` on the upload's institution.\n","tags":["User Files"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"fileId","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"The Upload `_id` to swap the asset for"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"The replacement asset (max 500 MB)"},"updateOriginalName":{"type":"boolean","description":"When true, rewrite the displayed `originalname` to the uploaded file's name. The on-disk `filename` and `file_url` always stay stable.","default":false}}}},"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"URL to download the replacement asset from"},"updateOriginalName":{"type":"boolean","description":"When true, rewrite `originalname` to the URL's trailing path segment.","default":false}}}}}},"responses":{"200":{"description":"Asset swapped successfully. Embeddings are preserved.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string"},"data":{"type":"object","description":"Updated Upload document"}}}}}},"400":{"description":"Bad request (missing input, invalid id, SSRF, etc.)"},"403":{"description":"Not authorized to edit this upload"},"404":{"description":"Upload not found"},"500":{"description":"Swap failed (filesystem error)"}}}},"/api/user/histories":{"post":{"summary":"Retrieve user conversation histories","description":"Fetches conversation history records based on specified filters. Results are sorted newest-first by the database, then reversed to oldest-first before returning. Long strings in inputs/outputs are trimmed to 200 chars. Tool responses are truncated to 80 chars unless tools=true.","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryQuery"},"example":{"limit":5,"course_id":1750532703472,"search":"deploy","tools":false}}}},"responses":{"200":{"description":"Successfully retrieved conversation histories","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoriesResponse"},"examples":{"successResponse":{"summary":"Successful response with history dialogues","value":{"success":true,"data":[{"id":"688b024f7db6fe6e921399e3","created":"2025-07-01T12:00:00.000Z","credits":1,"usage":150,"discount":0,"institution":"60d5ec49f1b2c80015a4d1a1","user":"60d5ec49f1b2c80015a4d1a2","favorite":false,"forgotten":false,"role_id":"60d5ec49f1b2c80015a4d1a3","role_name":"Student","thumbUpDown":"up","conversation_model":"gpt-4o","success":true,"in":{"input":"How are you?"},"out":{"outputs":["I am doing wonderful, thank you for asking..."]},"assistant":{"_id":"60d5ec49f1b2c80015a4d1a4","name":"My Assistant","liked_count":5,"picture_url":"https://example.com/avatar.png"}}]}}}}}},"400":{"description":"Bad request or query error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error getting histories invalid ObjectId"},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}}},"tags":["History"]}},"/api/user/favorites":{"post":{"summary":"Retrieve user's favorite dialogues","description":"Returns history records marked as favorites by the authenticated user. Uses the same HistoryRecord response shape as /histories. Results are trimmed (inputs/outputs to 200 chars, tool responses to 80 chars). Limited to 1000 records.","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FavoritesQuery"},"example":{"institution":"60d5ec49f1b2c80015a4d1a1"}}}},"responses":{"200":{"description":"Successfully retrieved favorite history dialogues","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FavoritesResponse"},"examples":{"successResponse":{"summary":"Successful response with favorites","value":{"success":true,"data":[{"id":"688b024f7db6fe6e921399e3","created":"2025-07-01T12:00:00.000Z","favorite":true,"favorite_name":"Deployment Guide","in":{"input":"How are you?"},"out":{"outputs":["I am doing wonderful, thank you for asking..."]},"assistant":{"_id":"60d5ec49f1b2c80015a4d1a4","name":"My Assistant","liked_count":5}}]}}}}}},"400":{"description":"Bad request or query error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error getting favorites invalid ObjectId"},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}}},"tags":["History"]}},"/api/user/courses":{"post":{"summary":"Retrieve user conversations","description":"Gets a grouped list of all conversations (courses) for the authenticated user. Each course includes its history count, last dialogue date, and the most recently used assistant (so callers can restore conversation context on the next turn). Courses with course_id=0 are filtered out of results. Institution scoping: pass an ObjectId to scope to a twin, explicit null/empty to scope to personal history only, or omit the field to fall back to the user's current institution.","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoursesQuery"},"example":{"institution":"60d5ec49f1b2c80015a4d1a1"}}}},"responses":{"200":{"description":"Successfully retrieved user courses","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseListResponse"},"examples":{"successResponse":{"summary":"Successful response with courses","value":{"success":true,"data":[{"course_id":1750532703472,"course_name":"Conversation 123","history_count":12,"last_dialogue_date":"2025-07-01T12:00:00.000Z","assistant":{"_id":"60d5ec49f1b2c80015a4d1a4","name":"My Assistant","liked_count":5,"picture_url":"https://example.com/avatar.png"}}]}}}}}},"400":{"description":"Bad request or query error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error getting courses invalid ObjectId"},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}}},"tags":["History"]}},"/api/user/history/{id}":{"put":{"summary":"Update a history record","description":"Updates a user's history record with the provided properties. Accepts any valid history field in the body, not just favorite and forgotten. Returns the full updated document.","security":[{"apiKeyAuth":[]}],"tags":["History"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The ObjectId of the history record to update","example":"687e770f93c79717445a0497"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHistoryRequest"}}}},"responses":{"200":{"description":"History record successfully updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHistoryResponse"}}}},"400":{"description":"Bad request - empty body or update error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid Properties"},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}},"404":{"description":"History record not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not found!"}}}}}}}}},"/api/user/clearHistory":{"post":{"summary":"Clear history records (soft delete)","description":"Soft-deletes history records by setting forgotten=true. Two operation modes: (1) course_id deletes a conversation within the resolved institution scope and detaches its favorites, (2) id deletes a single record. Favorites in a course are detached (course_id/course_name removed) rather than deleted. course_id mode supports institution scoping (ObjectId / explicit null = personal / omitted = user.institution fallback); allInstitutions:true is rejected with 400.","security":[{"apiKeyAuth":[]}],"tags":["History"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClearHistoryRequest"},"examples":{"clearCourse":{"summary":"Clear a specific conversation in the current twin (fallback)","value":{"course_id":1750532703472}},"clearCourseInTwin":{"summary":"Clear a conversation explicitly scoped to a twin","value":{"course_id":1750532703472,"institution":"60d5ec49f1b2c80015a4d1a1"}},"clearCoursePersonal":{"summary":"Clear a personal-history conversation (regardless of user.institution)","value":{"course_id":1750532703472,"institution":null}},"clearUnassigned":{"summary":"Sweep the unassigned bucket (course_id 0/null/missing)","value":{"course_id":0}},"clearSingle":{"summary":"Clear a single history record","value":{"id":"688b024f7db6fe6e921399e3"}}}}}},"responses":{"200":{"description":"History records successfully cleared","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClearHistoryResponse"},"examples":{"clearCourseResponse":{"summary":"Response when clearing by course_id","value":{"success":true,"message":"History updated!"}},"clearSingleResponse":{"summary":"Response when clearing a single record by id","value":{"success":true,"message":"History updated!"}}}}}},"400":{"description":"Bad request - invalid course_id (must be a number), malformed institution ObjectId, or allInstitutions:true was supplied (not supported on course-level destructive operations).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"allInstitutions is not supported on course-level destructive operations."},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}},"403":{"description":"Forbidden - the supplied institution ObjectId is not an active UserInstitution membership for this user.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"You are not a member of this institution"}}}}}}}}},"/api/user/renameHistory":{"post":{"summary":"Rename a conversation or favorite","description":"Two modes of operation: (1) Rename a conversation by providing course_id and course_name - updates all history records in that conversation within the resolved institution scope. (2) Rename a favorite by providing id and favorite_name - updates a single record. If favorite_name is falsy, the field is removed ($unset). Conversation renames support institution scoping (ObjectId / explicit null = personal / omitted = user.institution fallback); allInstitutions:true is rejected with 400.","security":[{"apiKeyAuth":[]}],"tags":["History"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameHistoryRequest"},"examples":{"renameCourse":{"summary":"Rename a conversation","value":{"course_id":1750532703472,"course_name":"My Renamed Conversation"}},"renameFavorite":{"summary":"Rename a favorite","value":{"id":"688b024f7db6fe6e921399e3","favorite_name":"Deployment Guide"}},"clearFavoriteName":{"summary":"Remove a favorite name","value":{"id":"688b024f7db6fe6e921399e3","favorite_name":""}}}}}},"responses":{"200":{"description":"Record(s) successfully renamed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameHistoryResponse"},"examples":{"courseRenamed":{"summary":"Conversation renamed","value":{"success":true,"message":"Conversation renamed!"}},"favoriteRenamed":{"summary":"Favorite renamed","value":{"success":true,"message":"Favorite renamed!"}}}}}},"400":{"description":"Bad request - invalid course_id type, malformed institution ObjectId, missing parameters, or allInstitutions:true was supplied.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid parameters. You must either rename a conversation or rename a favorite."},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}},"403":{"description":"Forbidden - the supplied institution ObjectId is not an active UserInstitution membership for this user.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"You are not a member of this institution"}}}}}}}}},"/api/user/summarizeHistory":{"post":{"summary":"Generate an AI summary title for a history record or conversation","description":"Uses AI to generate a concise title that describes the benefit or outcome of the conversation. The AI provider is determined by the institution's summaryModel configuration. When 'id' is supplied, reads a single history record (input, inputs, output, outputs, code) and returns up to 200 chars. When 'course_id' is supplied, summarizes the first 2 + last 3 dialogues of the conversation within the resolved institution scope (all dialogues if ≤5 total) and returns up to 50 chars. course_id mode supports institution scoping (ObjectId / explicit null = personal / omitted = user.institution fallback); allInstitutions:true is rejected with 400.","security":[{"apiKeyAuth":[]}],"tags":["History"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummarizeHistoryRequest"},"example":{"id":"688b024f7db6fe6e921399e3"}}}},"responses":{"200":{"description":"Summary successfully generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummarizeHistoryResponse"},"examples":{"successResponse":{"summary":"AI-generated title returned","value":{"success":true,"message":"Summary generated","data":"How to deploy a Node.js application to AWS with Docker"}}}}}},"400":{"description":"Bad request - missing id/course_id, invalid id, no dialogues for course_id, malformed institution ObjectId, allInstitutions:true, or AI provider error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid parameter. You must supply a valid id or course_id."},"error":{"type":"object"}}}}}},"401":{"description":"Unauthorized - user not found or invalid token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required!"}}}}}},"403":{"description":"Forbidden - the supplied institution ObjectId is not an active UserInstitution membership for this user.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"You are not a member of this institution"}}}}}}}}},"/api/user/userInstitution/{id}":{"delete":{"summary":"Remove user institution membership","description":"Removes a user's membership from an institution. If removing the currently active profile, the user is automatically switched to another available membership.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The user institution ID to remove (must be a valid ObjectId)"}],"responses":{"200":{"description":"Membership removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"You left successfully!"}}}}}},"400":{"description":"Invalid user institution Id or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid user institution Id"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"User does not own this membership","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid Membership. Not yours!"}}}}}},"404":{"description":"User institution not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found"}}}}}}}},"put":{"summary":"Update user institution preferences","description":"Updates user-institution settings. Accepts canvasApiToken (object to set, empty string to remove), lastConversation, favoriteAssistants, acceptedAccountPrivacyPolicyDate, and favorite. At least one valid field must be provided.","tags":["User Institutions"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The UserInstitution ObjectId to update","example":"6554560a7f05e300409043ba"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInstitutionUpdateRequest"}}}},"responses":{"200":{"description":"User institution preferences updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInstitutionUpdateResponse"}}}},"400":{"description":"Invalid user institution ID, no valid fields provided, or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Specify a valid property to update ex: (canvasApiToken, lastConversation, acceptedAccountPrivacyPolicyDate, favoriteAssistants)"}}}}}},"401":{"description":"User not found: authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"User-institution does not belong to the requesting user","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Yours!"}}}}}},"404":{"description":"User-institution record not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found"}}}}}}}}},"/api/user/userInstitutionCreatePersonal":{"post":{"summary":"Create personal institution","description":"Creates a new personal Digital Twin instance (UserInstitution with no institution) for the user. Does not return the created record.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Personal account created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Personal account created !"}}}}}},"400":{"description":"Error creating personal account (e.g. duplicate key violation)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to create personal account <error details>"},"error":{"type":"object"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}}}},"/api/user/institutionJoin":{"put":{"summary":"Join an institution","description":"Joins the user to an institution. If the user is not the referrer, membership requires approval and an email is sent to the referrer. If the user already has a membership for the institution, the existing record is returned. Credit awards may be issued on join if configured.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["institution","referrer"],"properties":{"institution":{"type":"string","description":"The institution ObjectId to join","example":"60d5ec49f1b2c72b9c8e4d3a"},"referrer":{"type":"string","description":"The referrer user ObjectId. If different from the current user, the membership is set to pending and requires approval.","example":"60d5ec49f1b2c72b9c8e4d3b"}}}}}},"responses":{"200":{"description":"Joined successfully or membership already exists","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"ui":{"type":"object","description":"The created or existing UserInstitution document","properties":{"_id":{"type":"string"},"user":{"type":"string"},"institution":{"type":"string"},"accountType":{"type":"string","enum":["super","user"]},"referrer":{"type":"string"},"status":{"type":"string","enum":["active","pending"]},"creditAwarded":{"type":"number","description":"Credits awarded on join (only if institution has credit awards configured)"},"creditAwardedDate":{"type":"string","format":"date-time","description":"Date credits were awarded (only if credits were awarded)"}}},"message":{"type":"string","example":"Joined successfully"}}}}}},"400":{"description":"Invalid input or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"One of: 'Authentication Required', 'Invalid Institution', 'Invalid Referrer', or error details"}}}}}},"404":{"description":"Referrer or institution not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Referrer not found!' or 'Institution not found'"}}}}}}}}},"/api/user/institutionCreateJoinId":{"post":{"summary":"Create or retrieve institution join code","description":"Generates a new join code (UUID v4) for the specified institution, or returns the existing one if already set. Requires admin or super privileges.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["institutionId"],"properties":{"institutionId":{"type":"string","description":"The institution ObjectId to create/retrieve a join code for","example":"60d5ec49f1b2c72b9c8e4d3a"}}}}}},"responses":{"200":{"description":"Join code created or retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"joinId":{"type":"string","description":"The join code UUID","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}},"message":{"type":"string","example":"Successful joinId"}}}}}},"400":{"description":"Invalid institution or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid institution"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Unauthorized - user-level accounts cannot create join codes","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Unauthorized for users"}}}}}},"404":{"description":"Institution not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found"}}}}}}}}},"/api/user/institutionSendInvite":{"post":{"summary":"Email an invite link to a recipient","description":"Composes an invitation email containing the institution's join link and sends it to the supplied email address. Requires admin or super privileges. Reuses the institution's existing joinId or generates one on demand.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["institutionId","email"],"properties":{"institutionId":{"type":"string","description":"The institution ObjectId to invite the recipient to","example":"60d5ec49f1b2c72b9c8e4d3a"},"email":{"type":"string","format":"email","description":"Recipient email address","example":"newuser@example.com"}}}}}},"responses":{"200":{"description":"Invitation email sent","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Invitation sent"}}}}}},"400":{"description":"Invalid institution, invalid email address, or send failure","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid email address"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Unauthorized - user-level accounts cannot send invitations","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Unauthorized for users"}}}}}},"404":{"description":"Institution not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found"}}}}}}}}},"/api/user/institutionByJoinId":{"post":{"summary":"Get institution by join code","description":"Retrieves institution information using a join code. Includes populated account data if available.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["joinId"],"properties":{"joinId":{"type":"string","description":"The institution's join code","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}}}}},"responses":{"200":{"description":"Institution found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"institution":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"ainame":{"type":"string"},"joinId":{"type":"string"},"picture":{"type":"string"},"picture_animated":{"type":"string"},"picture_bg":{"type":"string"},"picture_dark_bg":{"type":"string"},"about":{"type":"string"},"account":{"type":"object","description":"Populated account data (only present if institution has an account)","properties":{"_id":{"type":"string"},"name":{"type":"string"}}}}}}}}}},"400":{"description":"Invalid properties or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid properties"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"404":{"description":"Institution not found for the given join code","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid 'joinId' parameter. Institution not found!"}}}}}}}}},"/api/user/availableInstitutions":{"post":{"summary":"Get available institutions","description":"Retrieves a list of institutions the user can join. Results are wrapped in a UserInstitution-like structure with nested institution_data and account_data. Visibility rules depend on user account type and allowJoining/account settings. Optionally filters by a search term (matched against name, ainame, or account name).","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"search":{"type":"string","description":"Optional search term to filter institutions by name, ainame, or account name (case-insensitive)","example":"Biology"}}}}}},"responses":{"200":{"description":"Available institutions retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"entitlements":{"type":"array","items":{"type":"object","properties":{"_id":{"type":"string","description":"Sequential index (as string)"},"user":{"type":"string","description":"The requesting user's ID (as string)"},"institution":{"type":"string","description":"The institution ID (as string)"},"accountType":{"type":"string","description":"The user's account type"},"institution_data":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"ainame":{"type":"string"},"status":{"type":"string"},"credits":{"type":"number"},"poolCredits":{"type":"boolean"},"rtEnabled":{"type":"boolean"},"rtAdminOnly":{"type":"boolean"},"rtVoice":{"type":"string"},"contactEmail":{"type":"string"},"picture":{"type":"string"},"picture_animated":{"type":"string"},"css":{"type":"string"},"about":{"type":"string"},"questionType":{"type":"string"},"allowJoining":{"type":"string","enum":["disabled","public","account"]},"joiningAdminOnly":{"type":"boolean"}}},"account_data":{"type":"object","description":"Populated account data (empty object if no account)","properties":{"_id":{"type":"string"},"name":{"type":"string"},"managerEmail":{"type":"string"},"status":{"type":"string"}}},"entitlements":{"type":"array","description":"Always an empty array","items":{"type":"object"}}}}},"totalAvailable":{"type":"integer","description":"Total number of available institutions before search filtering","example":42}}}}}},"400":{"description":"Error getting institutions","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error getting institutions <error details>"},"error":{"type":"object"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}}}},"/api/user/approveUid":{"post":{"summary":"Approve a pending membership","description":"Approves a pending user institution membership. The caller must be the referrer (req.body.ref must match the authenticated user). On approval, the membership status is set to active and an approval email is sent to the user.","tags":["Membership"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ref","uI"],"properties":{"ref":{"type":"string","description":"The referrer user ID. Must match the authenticated user's ID.","example":"60d5ec49f1b2c72b9c8e4d3b"},"uI":{"type":"string","description":"The UserInstitution ObjectId to approve (must be a valid ObjectId)","example":"60d5ec49f1b2c72b9c8e4d3c"}}}}}},"responses":{"200":{"description":"Membership approved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"uI":{"type":"object","description":"The approved UserInstitution document (populated with user and institution)","properties":{"_id":{"type":"string"},"user":{"type":"object","description":"Populated user document"},"institution":{"type":"object","description":"Populated institution document"},"status":{"type":"string","example":"active"}}},"message":{"type":"string","example":"Membership approved"}}}}}},"400":{"description":"Invalid input or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Invalid Referrer', 'Invalid UI', or error details"}}}}}},"401":{"description":"Authentication Required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Request can only be approved by the referrer","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Request can only be approved by referrer"}}}}}},"404":{"description":"UserInstitution not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found"}}}}}}}}},"/api/user/memory":{"get":{"tags":["Memory"],"summary":"List the user's memory parameters (personal + shared in their instances).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/UserMemoryRow"}}}}}}}}}},"/api/user/memory/{id}":{"patch":{"tags":["Memory"],"summary":"Edit a memory parameter (whitelisted fields only).","description":"Whitelist: key_value, key_description, shared. Author can always edit\nown rows; non-author can edit shared rows iff super or holds\ninstitutions.edit on that institution.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"key_value":{"type":"string"},"key_description":{"type":"string"},"shared":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/UserMemoryRow"}}}}}}}},"delete":{"tags":["Memory"],"summary":"Hard-delete a single memory parameter.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/user/memory/personal":{"delete":{"tags":["Memory"],"summary":"Bulk hard-delete the user's own personal memory rows (filter-aware).","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}},"namespace":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/user/memory/institution/{id}":{"delete":{"tags":["Memory"],"summary":"Bulk hard-delete shared rows for an instance (requires institutions.edit).","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}},"namespace":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/user/data/stats":{"get":{"summary":"Personal-scope data counts","description":"Returns counts of personal-scope dialogues, uploads (with byte sum), and memory entries. Used by the My Data panel's initial render and by the post-delete refresh. Hits existing indexes — no `$facet` (DocumentDB-safe).","tags":["My Data"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Stats snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyDataStats"}}}},"500":{"description":"Mongo aggregation failed; check server logs"}}}},"/api/user/data/export":{"post":{"summary":"Request a ZIP-by-email export of personal data","description":"Rate-limited to 1 request per user per hour. Returns 202 immediately and kicks off a background build that streams the ZIP to `/uploads/<userId>/gdpr-export-<ts>.zip` and emails a 30-day-signed download link to the user's address. Contents: README.txt + dialogues.csv + uploads.csv + memory.csv + assets/. The ZIP reflects state at job start; concurrent deletes during the build are best-effort. If the build fails (disk full, mail relay down, etc.) a failure email is sent and the partial ZIP is removed.","tags":["My Data"],"security":[{"apiKeyAuth":[]}],"responses":{"202":{"description":"Export queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyDataExportResponse"}}}},"401":{"description":"Authentication required"},"429":{"description":"Rate-limited (more than 1 export request per user per hour)"},"500":{"description":"Pre-202 error (user lookup, public-dir mkdir, etc.); the background build's own failures are reported by email, not as HTTP errors."}}}},"/api/user/data":{"delete":{"summary":"Scoped soft-delete of personal data","description":"Atomic per-class soft-delete. At least one of `dialogues` / `vault` must be true. Personal-scope only — institution rows are filtered out before any mutation. Idempotent: re-running returns zeros once everything is already soft-deleted. Vault deletes go through `rag.fileDelete` to clear embeddings + KAG cascade + physical file before flipping `status='deleted'` and stamping `deleted_at`. Per-upload failures from rag.fileDelete are collected in `uploadsFailed` and the status flip is skipped on those rows so a retry can re-attempt the physical removal.","tags":["My Data"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyDataDeleteRequest"}}}},"responses":{"200":{"description":"Counts of what was deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyDataDeleteResponse"}}}},"400":{"description":"Neither dialogues nor vault was true"},"401":{"description":"Authentication required"},"500":{"description":"Mongo update / rag.fileDelete fatal error"}}}},"/api/user/me":{"put":{"summary":"Update user profile or change password","description":"Updates the current user's profile fields or changes the user's password. When `resetCodeId` is present in the request body, the endpoint operates in password-change mode and requires `password` (current) and `newPassword` fields. Otherwise, it updates profile fields validated against the authorized properties list (email, fname, lname, picture, remember_history_count, use_location, use_stt, dark_mode, pin_ui, showSideBar, galleryAsGrid, ragOnlySearch, ragIgnore, ragKagMode, browser_voice, browser_voices, mustChangePassword, updatePasswordOnSSO, rt_voice, gemini_rt_voice, xai_rt_voice, mfaEnabled). Any properties not in this list are rejected.\n","tags":["User"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"title":"Profile Update","type":"object","description":"Any combination of authorized user properties. Only fields from the authorized list are accepted.","properties":{"email":{"type":"string","format":"email","description":"User email address"},"fname":{"type":"string","description":"First name"},"lname":{"type":"string","description":"Last name"},"picture":{"type":"string","description":"Profile picture URL"},"remember_history_count":{"type":"integer","description":"Number of history items to remember"},"use_location":{"type":"boolean","description":"Whether to use location services"},"use_stt":{"type":"boolean","description":"Whether speech-to-text is enabled"},"dark_mode":{"type":"boolean","description":"Whether dark mode is enabled"},"pin_ui":{"type":"boolean","description":"Whether the UI sidebar is pinned"},"showSideBar":{"type":"boolean","description":"Whether to show the sidebar"},"galleryAsGrid":{"type":"boolean","description":"Whether to display gallery as grid"},"ragIgnore":{"type":"boolean","description":"Knowledge — retrieval mode (axis 1 of 2). When `true`, retrieval is\nskipped entirely and the LLM answers from its training only.\nTakes precedence over `ragKagMode` and `ragOnlySearch`.\nMaps to the UI's \"Disabled\" Knowledge mode.\n"},"ragKagMode":{"type":"boolean","description":"Knowledge — retrieval mode (axis 1 of 2). When `true` AND retrieval\nis on (`ragIgnore=false`) AND the user/institution is KAG-eligible,\nthe knowledge-graph leg runs alongside the dense vector leg and the\ntwo are merged via Reciprocal Rank Fusion. NB. KAG is always an\naugmentation on top of RAG — there is no \"KAG without RAG\" mode.\nMaps to the UI's \"RAG + KAG Fusion\" Knowledge mode.\nWhen `false` with `ragIgnore=false`, behaviour is \"RAG Only\"\n(dense vector retrieval only).\n"},"ragOnlySearch":{"type":"boolean","description":"Knowledge — output mode (axis 2 of 2, orthogonal to ragIgnore /\nragKagMode). When `true` AND retrieval is on, Pria returns the\nraw vault chunks (\"Search Only\") instead of an LLM-rewritten\nanswer. Combinable with `ragKagMode` (graph chunks included)\nand with vanilla RAG. No effect when `ragIgnore=true`.\nKnowledge mode combinations (frontend view):\n  • ragIgnore=true                                  → Disabled\n  • ragIgnore=false, ragKagMode=false, ragOnly=false → RAG Only\n  • ragIgnore=false, ragKagMode=true,  ragOnly=false → RAG + KAG Fusion\n  • ragIgnore=false, ragKagMode=false, ragOnly=true  → RAG Only + Search Only\n  • ragIgnore=false, ragKagMode=true,  ragOnly=true  → RAG + KAG Fusion + Search Only\n"},"browser_voice":{"type":"string","description":"Selected browser voice for TTS"},"browser_voices":{"type":"array","items":{"type":"string"},"description":"Available browser voices"},"mustChangePassword":{"type":"boolean","description":"Whether user must change password on next login"},"updatePasswordOnSSO":{"type":"boolean","description":"Reset password on SSO login"},"rt_voice":{"type":"string","description":"Real-time voice selection (OpenAI Realtime)"},"gemini_rt_voice":{"type":"string","description":"Real-time voice selection (Gemini Live)"},"xai_rt_voice":{"type":"string","description":"Real-time voice selection (xAI Realtime)"},"mfaEnabled":{"type":"boolean","description":"Whether email multi-factor authentication is enabled for this user"}}},{"title":"Password Change","type":"object","required":["resetCodeId","password","newPassword"],"properties":{"resetCodeId":{"type":"string","description":"Reset code ID that must match the user's stored resetCodeId to authorize the password change"},"password":{"type":"string","description":"Current password for verification"},"newPassword":{"type":"string","minLength":6,"description":"New password (must be at least 6 characters)"}}}]}}}},"responses":{"200":{"description":"Profile updated or password changed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","description":"Either 'Profile updated!' or 'Password Changes Successfully'"}}}}}},"400":{"description":"Bad request - empty body, or missing/invalid newPassword during password change","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"One of: 'Invalid properties', 'Please enter your existing password', 'New password must be at least 6 alpha numeric characters long'"}}}}}},"401":{"description":"Unauthorized - user not found or account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - invalid resetCodeId, wrong current password, or unauthorized properties in request body","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"One of: 'Invalid or unauthorized change password request (resetCodeId)', 'Password doesn't match with your current value!', or 'Unauthorized properties detected: ...' when body contains fields not in the authorized list"}}}}}},"404":{"description":"Not found - user record not found during update","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"Either 'Not Found', 'User not found!', or 'Could not update your profile! <error>'"}}}}}}}}},"/api/user/institution":{"put":{"summary":"Update institution settings","description":"Updates the current user's institution configuration. No admin privilege check is enforced -- any authenticated user with an active institution can call this endpoint, though some properties (such as `creditCaps`) require the institutions.edit entitlement and are silently dropped otherwise. Admins/managers may set `creditCaps` (per-user quotas) on their institution when the account has not enabled caps; when the account owns caps `creditCaps` is ignored (read-only inherited). The request body is validated against the institution authorized properties list (conversationModel, maxCompletionTokens, reasoningEffort, extendedContext, about, personalisationAsked, disableToolUseInstructions, kmeanScore, ragLimitChunks, toolsDisabled, compactHistory, displayThinkingDetails, displayAgentDetails, displayRagSearchDetails, displayThinkingExecution, displayToolExecution, enableModeration, alwaysCiteSources, toolResultsMaxChars, rtEnabled, rtAdminOnly, rtTextInputEnabled, rtModel, rtVoice, gemini_rt_voice, xai_rt_voice, rtVADEagerness, rtNoiseReduction, rtTranscriptionLanguage, disableFileUploadForUser, disableAudioNotesForUser, disableClipboardForUser, locationEnabled, guestUI, theme, picture, ainame, assistantsDisabled). Any properties not in this list are rejected with a 400.\n","tags":["Institutions"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Any combination of authorized institution properties","properties":{"conversationModel":{"type":"string","description":"Default conversation model for the institution"},"maxCompletionTokens":{"type":"integer","description":"Maximum completion tokens for AI responses"},"reasoningEffort":{"type":"string","description":"Reasoning effort level (none, low, medium, high, max)"},"extendedContext":{"type":"boolean","description":"Whether extended context is enabled"},"about":{"type":"string","description":"About text for the institution"},"personalisationAsked":{"type":"boolean","description":"Whether personalisation has been asked"},"disableToolUseInstructions":{"type":"boolean","description":"Whether to disable tool use instructions"},"kmeanScore":{"type":"number","description":"K-means score threshold for RAG"},"ragLimitChunks":{"type":"integer","description":"Maximum number of RAG chunks to retrieve"},"toolsDisabled":{"type":"boolean","description":"Whether tools are disabled for AI conversations"},"enableModeration":{"type":"boolean","description":"Route incoming prompts through the moderation model before they reach the main LLM"},"alwaysCiteSources":{"type":"boolean","description":"Add an explicit citation directive to the system prompt so the model footnotes RAG / web sources in its answers"},"toolResultsMaxChars":{"type":"integer","description":"Character cap for individual tool / scrape / vault results before they are trimmed and saved to the IP Vault. Default 60000.","minimum":1000,"maximum":200000},"rtEnabled":{"type":"boolean","description":"Enable Convo / Speech-to-Speech (realtime voice) mode for this instance"},"rtAdminOnly":{"type":"boolean","description":"Restrict Convo mode to admin users only"},"rtTextInputEnabled":{"type":"boolean","description":"Allow text input alongside speech while in Convo mode"},"rtModel":{"type":"string","description":"System model id for Convo / Speech-to-Speech (provider auto-derived from the model id)"},"rtVoice":{"type":"string","description":"OpenAI Realtime voice (alloy, ash, ballad, coral, echo, sage, shimmer, verse, cedar, marin)"},"gemini_rt_voice":{"type":"string","description":"Gemini Live voice (Puck, Charon, Kore, etc.)"},"xai_rt_voice":{"type":"string","description":"xAI realtime voice (eve, ara, rex, sal, leo)"},"rtVADEagerness":{"type":"string","enum":["low","medium","high"],"description":"OpenAI realtime voice-activity-detection eagerness"},"rtNoiseReduction":{"type":"string","enum":["","near_field","far_field"],"description":"OpenAI realtime noise reduction profile"},"rtTranscriptionLanguage":{"type":"string","description":"ISO 639-1 language hint for OpenAI realtime transcription (blank = auto-detect)"},"disableClipboardForUser":{"type":"boolean","description":"Hide clipboard controls from non-admin users in this institution"},"locationEnabled":{"type":"boolean","description":"Allow this institution's users to opt-in to sharing browser geolocation with the assistant for location-aware answers. Defaults to true."},"guestUI":{"type":"boolean","description":"Strip the UI down to a chat-only minimum surface for non-admin users"},"theme":{"type":"string","description":"Forced UI theme for all users in this institution. '' = user directed, 'dark' = force dark mode, 'light' = force light mode","enum":["","dark","light"]},"creditCaps":{"allOf":[{"$ref":"#/components/schemas/InstitutionCreditCaps"}],"description":"Per-user quota configuration. Settable by admins/managers with the institutions.edit entitlement when the account has not enabled caps; ignored (read-only inherited) when the account owns caps."}}}}}},"responses":{"200":{"description":"Institution updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Institution updated!"}}}}}},"400":{"description":"Bad request - empty body, user has no institution, unauthorized properties, or generic error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"One of: 'Invalid properties', 'Select an institution!', 'Unauthorized properties detected: ...', or 'Error'"}}}}}},"401":{"description":"Unauthorized - user not found or account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"404":{"description":"Institution not found - institution ID on user does not match any record","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Institution not found!"}}}}}}}}},"/api/user/institutionAbout":{"post":{"summary":"Fetch lazy-loaded institution about markdown","description":"Returns the `about` markdown for a single institution, gated by an active UserInstitution membership for the caller. Used by the Gallery card flip and About modal so the bulk entitlements payload (POST /api/user/refresh/entitlements) does not have to ship the about text for every institution the user belongs to.\n","tags":["Institutions"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["institution"],"properties":{"institution":{"type":"string","description":"Institution _id (must be a valid ObjectId)","example":"678e99009db586c5e9f6c903"}}}}}},"responses":{"200":{"description":"About markdown returned (empty string when the institution has no description)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"about":{"type":"string","description":"Markdown body of the institution's about; empty string when unset."},"ainame":{"type":"string","description":"Institution's AI display name; empty string when unset."}}}}}},"400":{"description":"Missing or invalid institution id","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Missing institution id"}}}}}},"403":{"description":"Caller does not hold an active UserInstitution for the requested institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not authorized for this institution"}}}}}},"500":{"description":"Internal error while loading the institution"}}}},"/api/user/unlinkContextId/{id}":{"put":{"summary":"Unlink LTI context from user institution","description":"Removes the LTI context association from a user institution, allowing reassociation with a different Digital Twin. Validates that the user institution exists, belongs to the authenticated user, has an LTI context linked, belongs to an institution, and the institution record exists. Also removes the context ID from the parent institution's ltiContextIds array.\n","tags":["User Institutions"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The user institution ID to unlink (must be a valid ObjectId)"}],"responses":{"200":{"description":"LTI context removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"LTI context removed."}}}}}},"400":{"description":"Bad request - invalid ObjectId, user institution not found, no LTI context linked, institution missing on record, or generic error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"One of: 'User Institution not found', 'No LTI context linked to this digital twin', 'LTI context must be for an institution', or 'Failed to find userInstitution'"},"message":{"type":"string","description":"'Invalid user institution Id' (for invalid ObjectId) or 'Failed to find userInstitution' (for catch-all errors)"}}}}}},"401":{"description":"Unauthorized - user not found or the user institution does not belong to the authenticated user","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"Either 'Authentication required' (message field) or 'This membership is not your!' (error field)"}}}}}},"404":{"description":"Not found - the parent institution record does not exist in the database","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"Not found"}}}}}}}}},"/api/user/userInstitutionCredit/{id}":{"post":{"summary":"Credit user institution with tokens","description":"Transfers credits from the parent account to the institution. Requires admin or super account type. Validates that the user institution exists, belongs to the authenticated user, matches the user's current institution, the institution is linked to a parent account, and the account has sufficient credits. Creates an AccountTransfer record to log the transaction. Defaults to 1000 credits if not specified.\n","tags":["User Institutions"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The user institution ID to credit (must be a valid ObjectId)"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"credits":{"type":"integer","description":"Number of credits to transfer (defaults to 1000 if omitted)","example":1000}}}}}},"responses":{"200":{"description":"Credits transferred successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Transfer successfuly executed and recorded!"}}}}}},"400":{"description":"Bad request - user has no institution, invalid ObjectId, user institution not found, account not found, insufficient credits, or generic error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"One of: 'Invalid User Institution', 'Invalid request' (UserInstitution not found), 'Invalid Account', 'Account does not have enough credits. Requires 1,000.'"},"message":{"type":"string","description":"'Invalid user institution Id' (for invalid ObjectId) or 'Error crediting institution <error>' (for catch-all errors)"}}}}}},"401":{"description":"Unauthorized - user not found, not an admin/super, user institution belongs to different user, institution missing on user institution, institution mismatch, or institution not linked to account","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"One of: 'Authentication Required', 'You must be an admin!', 'User Unauthorized!', 'Invalid or missing Instance!', 'User Unauthorized for this instance!', 'Instance must be linked to a parent account!'"}}}}}},"404":{"description":"Not found - account or institution not found during credit update","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"Either 'Account not found!' or 'Could not credit institution. Not found'"}}}}}}}}},"/api/user/uploadPicture":{"post":{"summary":"Upload profile picture","description":"Uploads a profile picture from base64-encoded image data. Saves the file to the user's home directory, creates a RAG upload record for the file, and updates the user's profile picture URL. Supported formats are png, jpeg, jpg, gif, and webp. Maximum file size is 5MB.\n","tags":["User"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["imageData"],"properties":{"imageData":{"type":"string","description":"Base64 encoded image data with data URI prefix. Supported formats: png, jpeg, jpg, gif, webp. Maximum decoded size: 5MB.","example":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="}}}}}},"responses":{"200":{"description":"Picture uploaded successfully. The file is saved to disk, a RAG upload record is created, and the user's profile picture URL is updated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"url":{"type":"string","description":"URL to access the uploaded picture","example":"https://hiimpria.ai/uploads/6430d02554cd4e00403e8b05/profile_1738332000000_abc12345.png"},"message":{"type":"string","example":"Picture has been uploaded to your document collection successfully"}}}}}},"400":{"description":"Bad request - missing imageData, imageData is not a string, invalid base64 format, unsupported image type, or file exceeds 20MB limit","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"One of: 'Image data is required', 'Invalid image data format. Expected base64 encoded image.', 'Image size exceeds 20MB limit'"}}}}}},"401":{"description":"Unauthorized - user not found or account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"500":{"description":"Internal server error - file system error, RAG upload failure, or other unexpected error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"'Failed to upload picture: <error>'"}}}}}}}}},"/api/user/customModels":{"post":{"summary":"Get custom AI models","description":"Retrieves the list of custom AI models available for the current institution","tags":["User"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Custom models retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomModelsResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/systemModels":{"post":{"summary":"Get system AI models","description":"Retrieves the list of system-wide AI models available","tags":["User"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"System models retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemModelsResponse"}}}},"401":{"description":"Unauthorized - Invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/questions":{"post":{"tags":["Questions"],"summary":"List active onboarding questions for the Persona Builder bank.","description":"Returns every active question whose `code` starts with the supplied\n`questionType` (currently always `PERSONA` — the single live bank),\nsorted by `position`. Used by the create-twin wizard at\n/my-profile/create. No pagination — the wizard renders the whole\nlist at once.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"questionType":{"type":"string","description":"Code prefix to filter on (e.g. \"PERSONA\"). Omit to return every active question."}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/OnboardingQuestion"}},"message":{"type":"string"}}}}}}}}},"/api/user/searchRag":{"post":{"summary":"Search RAG content for user using a specified query string","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"search":{"type":"string","description":"The search term to query RAG content"},"assistantId":{"type":"string","description":"Optional assistant ID to scope search to the assistant's RAG collections"}},"required":["search"]}}}},"responses":{"200":{"description":"Successful RAG search results returned","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"string"},"message":{"type":"string","example":"RAG Search results for example query"}}}}}},"400":{"description":"Validation error or server error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"Either 'search is required!' for missing search param, or 'Error searcing RAG ...' for server errors"},"error":{"type":"object","description":"Error object included only on caught exceptions"}}}}}},"401":{"description":"Authentication failure - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}},"security":[{"apiKeyAuth":[]}],"tags":["RAG"]}},"/api/user/settingByKey":{"post":{"summary":"Retrieve settings by key","description":"Fetches settings matching a specific key. When no institution is provided, returns settings where institution is null. Results are sorted by key descending and limited to 100 records.","security":[{"apiKeyAuth":[]}],"tags":["Setting"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"Setting key to search for (exact match)","example":"variable-2"},"status":{"type":"string","description":"Filter by status. When omitted, excludes settings with status 'deleted'.","enum":["active","inactive","deleted"],"example":"active"},"institution":{"type":"string","description":"Institution ID to scope the search. When omitted, returns settings where institution is null.","example":"6631915765bb0a94cfd6ca99"}}}}}},"responses":{"200":{"description":"Successfully retrieved settings","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/UserSetting"}}}}}}},"400":{"description":"Bad request - missing key, invalid institution ID, or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Key is required!"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}}}}},"/api/user/settings":{"post":{"summary":"Retrieve settings for the current institution","description":"Fetches settings scoped to the authenticated user's institution, with institution and user metadata. Results are sorted by key ascending and limited to 100 records. Requires the user to belong to an institution.","security":[{"apiKeyAuth":[]}],"tags":["Setting"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"Filter by status. When omitted, excludes settings with status 'deleted'.","enum":["active","inactive","deleted"],"example":"active"},"key":{"type":"string","description":"Filter by exact key name","example":"variable-2"}}}}}},"responses":{"200":{"description":"Successfully retrieved settings","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/UserSetting"}}}}}}},"400":{"description":"Bad request - general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Error getting setting <details>"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - user does not belong to an institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"This functionality is reserved to instance users"}}}}}}}}},"/api/user/setting":{"post":{"summary":"Create a new instance variable","description":"Creates a new setting within the authenticated user's institution. The institution and user fields are auto-populated from the auth context. A duplicate key within the same institution returns a 400 error.","security":[{"apiKeyAuth":[]}],"tags":["Setting"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSettingRequest"}}}},"responses":{"200":{"description":"Setting successfully created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"$ref":"#/components/schemas/CreatedSetting"},"message":{"type":"string","example":"Setting created!"}}}}}},"400":{"description":"Bad request - missing properties, missing key, duplicate key, creation failure, or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Setting with the same key already exists!"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - user does not belong to an institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"This functionality is reserved to instance users"}}}}}}}}},"/api/user/setting/{settingId}":{"put":{"summary":"Update an instance variable","description":"Updates an existing setting by ID. Accepts any non-empty object as the request body; no specific fields are required. No institution ownership check is performed.","security":[{"apiKeyAuth":[]}],"tags":["Setting"],"parameters":[{"in":"path","name":"settingId","required":true,"schema":{"type":"string"},"description":"The unique identifier of the setting to update","example":"687e6ee393c79717445848de"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingRequest"}}}},"responses":{"200":{"description":"Setting successfully updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Setting updated!"}}}}}},"400":{"description":"Bad request - empty body or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Properties Required"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - user does not belong to an institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"This functionality is reserved to instance users"}}}}}},"404":{"description":"Setting not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Setting not found!"}}}}}}}},"delete":{"summary":"Delete an instance variable","description":"Permanently removes a setting by ID. The delete is scoped to the authenticated user's institution. Validates the ID format and checks that the setting exists before attempting deletion.","security":[{"apiKeyAuth":[]}],"tags":["Setting"],"parameters":[{"in":"path","name":"settingId","required":true,"schema":{"type":"string"},"description":"The unique identifier of the setting to delete","example":"687e6ee393c79717445848de"}],"responses":{"200":{"description":"Setting successfully deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Setting removed"}}}}}},"400":{"description":"Bad request - invalid ObjectId format, delete failed, or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid setting id"}}}}}},"401":{"description":"Unauthorized - user not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - user does not belong to an institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"This functionality is reserved to instance users"}}}}}},"404":{"description":"Setting not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Setting not found"}}}}}}}}},"/api/user/stripe/plan":{"post":{"summary":"Get current subscription plan","description":"Retrieves the current user's Stripe subscription plan details and status","tags":["Billing"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Plan retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetPlanResponse"}}}},"400":{"description":"Bad request (e.g., user does not exist)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error details"}}}}}},"401":{"description":"Unauthorized - invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/stripe/subscribe":{"post":{"summary":"Create subscription checkout session","description":"Creates a Stripe checkout session and returns the full session object. The client should redirect the user to the session URL to complete payment.","tags":["Billing"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeRequest"}}}},"responses":{"200":{"description":"Checkout session created successfully. Returns the full Stripe checkout session object.","content":{"application/json":{"schema":{"type":"object","description":"Full Stripe Checkout Session object (see Stripe API docs)","properties":{"id":{"type":"string","description":"Stripe checkout session ID","example":"cs_test_abc123"},"url":{"type":"string","format":"uri","description":"URL to redirect the user to for checkout"},"mode":{"type":"string","description":"Checkout mode (subscription or payment)"},"status":{"type":"string","description":"Session status"},"client_reference_id":{"type":"string","description":"Reference containing priceId and institutionId"}}}}}},"400":{"description":"Bad request (e.g., invalid price ID, Stripe unavailable, or user not found)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}}}},"401":{"description":"Unauthorized - invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/user/stripe/checkout-result":{"get":{"summary":"Get checkout session result","description":"Retrieves details of a completed Stripe checkout session for displaying a purchase confirmation screen. Verifies the session belongs to the authenticated user.","tags":["Billing"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"query","name":"session_id","required":true,"schema":{"type":"string"},"description":"Stripe checkout session ID (from the success URL redirect)","example":"cs_test_abc123"}],"responses":{"200":{"description":"Checkout result retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResultResponse"}}}},"400":{"description":"Missing session_id or Stripe API error","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"401":{"description":"Unauthorized - invalid or missing access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session does not belong to this user","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/api/user/refresh/profile":{"post":{"summary":"Refresh user profile data","description":"Retrieves the current user's full profile including populated institution, account, and Google OAuth status. Proactively refreshes expired Google tokens when a refresh_token is available. Returns a fresh JWT token (sliding session) — store this token to extend the session without re-authentication.","tags":["User"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Profile refreshed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshProfileResponse"}}}},"400":{"description":"General error during profile retrieval","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}},"401":{"description":"User not found or deleted: authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication required"}}}}}}}}},"/api/user/refresh/entitlements":{"post":{"summary":"Refresh user entitlements and permissions","description":"Retrieves the current user's entitlements across all institutions. Transforms populated institution/account into institution_data/account_data. Sorts by favorite flag, ainame, and institution name. Auto-cleans null-institution duplicate records. Returns accountManagerAccounts.","tags":["User"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Successfully refreshed user entitlements","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshEntitlementsResponse"}}}},"400":{"description":"General error during entitlements retrieval","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object"},"message":{"type":"string"}}}}}},"401":{"description":"User not found: authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication required"}}}}}}}}},"/api/user/userInstitutionSwitch/{id}":{"post":{"summary":"Switch user's active institution profile","description":"Switches the user's active institution profile. Updates the user's institution reference and plan (sdk for institution, free for personal). Also updates lastLogin and lastKA on the target user-institution record.","tags":["User Institutions"],"security":[{"apiKeyAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The UserInstitution ObjectId to switch to","example":"6554560a7f05e300409043ba"}],"responses":{"200":{"description":"Institution profile switched successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInstitutionSwitchResponse"},"example":{"success":true,"message":"Profile switched!"}}}},"400":{"description":"Invalid or missing user institution ID, or general error","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"}}}}}},"401":{"description":"User not found: authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"User-institution does not belong to the requesting user","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Request Unauthorized!"}}}}}},"404":{"description":"User-institution record not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Not Found!"}}}}}}}}},"/api/user/history/{id}/thinking":{"get":{"summary":"Lazy-fetch the bulk thinking and reasoning array for a single History record","description":"List endpoints (`/api/user/histories`, `/api/user/favorites`) project out the\nbulk `thinking` text and expose only `hasThinking` (boolean) + `thinkingCount`\n(number of reasoning rounds) for the UI summary badge. This endpoint returns\nthe full thinking array on demand, fired by the UI when the user expands the\nlightbulb `<details>` block.\n","tags":["User History"],"parameters":[{"name":"id","in":"path","required":true,"description":"History record ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Thinking array (may be empty)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"thinking":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"round-0"},"round":{"type":"integer","example":0},"text":{"type":"string"},"signature":{"type":"string"},"model":{"type":"string"},"durationMs":{"type":"integer"}}}}}}}}},"401":{"description":"Authentication required"},"404":{"description":"History record not found or not owned by the requesting user"}}}},"/api/user/history/{id}/ragSearch":{"get":{"summary":"Lazy-fetch the structured RAG-KAG retrieval segments for a single History record","description":"List endpoints (`/api/user/histories`, `/api/user/favorites`) project out the\nbulk `ragSearch` array (multi-KB per row) and expose only `hasRagSearch`,\n`ragSearchCount`, and `ragSearchMode` for the summary line. This endpoint\nreturns the full structured retrieval array on demand, fired by the UI when\nthe user expands the \"RAG/KAG Search Results\" `<details>` block.\n\nConfidential chunks arrive pre-redacted (≤100 chars + \"(rest is confidential)\"\nsuffix when the original exceeded the preview cap).\n","tags":["User History"],"parameters":[{"name":"id","in":"path","required":true,"description":"History record ID","schema":{"type":"string"}}],"responses":{"200":{"description":"ragSearch array (may be empty)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"ragSearch":{"type":"array","items":{"type":"object","properties":{"uploadId":{"type":"string"},"originalname":{"type":"string"},"chunkIndex":{"type":"integer","nullable":true},"score":{"type":"number","format":"float"},"length":{"type":"integer","description":"Original chunkText length (pre-redaction)"},"mode":{"type":"string","enum":["RAG","KAG"]},"chunkText":{"type":"string"},"confidential":{"type":"boolean"}}}}}}}}},"401":{"description":"Authentication required"},"404":{"description":"History record not found or not owned by the requesting user"}}}},"/api/user/history/{id}/audio":{"get":{"summary":"Read an assistant response aloud using the configured speech model","description":"Generates (or replays) spoken audio for the assistant response of a single\nHistory record using the institution's configured text-to-speech model.\nAvailable when read-aloud is enabled for the institution (or on the personal\nprofile for personal accounts).\n\nGenerated audio is cached server-side with a TTL, so repeat requests for the\nsame response replay instantly and are not billed again. A cache miss\nsynthesizes the speech and consumes credits proportional to the response\nlength (long responses are truncated to a maximum character cap).\n","tags":["User History"],"parameters":[{"name":"id","in":"path","required":true,"description":"History record ID","schema":{"type":"string"}},{"name":"voice","in":"query","required":false,"description":"Optional voice override. Defaults to the provider's standard voice.","schema":{"type":"string"}}],"responses":{"200":{"description":"The spoken audio. MP3 for most speech models; WAV for Grok TTS.","content":{"audio/mpeg":{"schema":{"type":"string","format":"binary"}},"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid history id"},"401":{"description":"Authentication required"},"403":{"description":"Read-aloud is not enabled for this user or institution"},"404":{"description":"History record not found or not owned by the requesting user"},"422":{"description":"The history record has no readable response text"},"501":{"description":"The configured speech model does not support read-aloud"}}}},"/api/user/tts/preview":{"get":{"summary":"Hear a short sample of a read-aloud voice","description":"Returns a short spoken sample so users can audition a voice from the\nread-aloud voice pickers before choosing it. The sample is generated\nwith the configured text-to-speech model, cached server-side, and is\nnot billed against credits.\n","tags":["User History"],"parameters":[{"name":"voice","in":"query","required":false,"description":"Catalog voice to sample. Defaults to the caller's effective voice.","schema":{"type":"string"}}],"responses":{"200":{"description":"The spoken sample. MP3 for most speech models; WAV for Grok TTS.","content":{"audio/mpeg":{"schema":{"type":"string","format":"binary"}},"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unknown voice"},"401":{"description":"Authentication required"},"403":{"description":"Account or membership is not active"},"501":{"description":"The configured speech model does not support read-aloud"}}}},"/api/user/tools":{"post":{"summary":"Get available tools for the authenticated user","description":"Retrieves the list of active tools available for the current user. Returns minimal tool information needed for Pria requests, including availability status for Google-integrated tools based on the user's institution settings and Google Cloud configuration.\n\nResults are sorted in ascending alphabetical order by tool name.\n\nFor Google-integrated tools (gmail, drive, calendar, sheets, docs, slides, meet, maps, classroom), the response includes `unavailable` and `unavailableReason` fields. Availability is determined by checking whether Google OAuth is configured, whether the institution has enabled Google Cloud for users, and whether an institution-level Google account is connected. Non-Google tools do not include these fields.\n","tags":["Tools"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"minimum":{"type":"boolean","description":"When true, omits the `instructions` field from each tool in the response. Useful for selector UIs that only need the identifier + short description.\n","example":true}}}}}},"responses":{"200":{"description":"Tools retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetToolsResponse"},"examples":{"withTools":{"summary":"Response with tools including Google availability fields","value":{"success":true,"data":[{"_id":"6631915765bb0a94cfd6ca99","name":"call_google_calendar","status":"active","description":"Access Google Calendar to manage events","rtEnabled":true,"rtOnly":false,"unavailable":false,"unavailableReason":""},{"_id":"6631915765bb0a94cfd6ca9a","name":"call_google_drive","status":"active","description":"Access Google Drive files","rtEnabled":false,"rtOnly":false,"unavailable":true,"unavailableReason":"Google Cloud not enabled for users"},{"_id":"6631915765bb0a94cfd6ca9b","name":"call_web_search","status":"active","description":"Search the web","rtEnabled":false,"rtOnly":false}]}},"noTools":{"summary":"Response when no active tools exist","value":{"success":true,"data":[],"message":"No Tools"}}}}}},"400":{"description":"Bad request - error retrieving tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetToolsErrorResponse"},"example":{"success":false,"error":{},"message":"Error getting tools Database connection failed"}}}},"401":{"description":"Unauthorized - user not found or invalid access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Authentication Required"}}}}}}},"/api/user/transcribe":{"post":{"summary":"Transient speech-to-text","description":"Accepts a single audio blob, dispatches it to the user's institution-\nconfigured STT provider via `getProviderForModel(user, 'audioAnalysisModel')`,\nand returns the transcript. Suitable for in-place dictation (e.g. the\nchat textarea mic icon) — explicitly does NOT persist `Upload`,\n`History`, or embedding rows.\n\nRate-limited 30 req / 60s / user (per-process token bucket). The\ngate also enforces the same `checkAudioNotesAllowed` predicate the\n`/audio-notes` route uses (guest mode + institutional opt-out).\n\nAccepted mimetypes: `audio/webm`, `audio/ogg`, `audio/mp4`,\n`audio/mpeg`, `audio/wav`. 25 MB ceiling covers ~120s of opus at\n128 kbps with headroom.\n","tags":["Transcription"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["audio"],"properties":{"audio":{"type":"string","format":"binary","description":"The audio blob to transcribe."},"language":{"type":"string","description":"Optional BCP-47 hint passed through to the provider (e.g. `en`, `fr`). Some providers ignore it."}}}}}},"responses":{"200":{"description":"Transcript returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscribeResponse"}}}},"400":{"description":"Missing / oversized audio file, or wrong mimetype."},"401":{"description":"Authentication required."},"403":{"description":"Audio-notes are disabled for this user / institution."},"429":{"description":"Rate-limited (>30 req / 60s for this user on this process).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Rate limit: 30/min"},"retryAfterMs":{"type":"integer"}}}}}},"502":{"description":"Upstream STT provider failed."}}}},"/api/user/uploads":{"post":{"summary":"Search and retrieve user uploaded files","description":"Retrieves a paginated list of user uploads with optional filtering by filename, status, vault type, and institution. Supports compact and lean response modes. When no vault is specified, legacy behavior applies: the user's personal files plus institution files are returned, and account-shared uploads from sibling institutions are appended.","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadSearchRequest"},"examples":{"basicSearch":{"summary":"Basic search excluding deleted files","value":{"lean":true,"status":{"$ne":"deleted"},"fileNameSearch":"report"}},"paginatedVaultSearch":{"summary":"Paginated search within a specific vault","value":{"vault":"instance","institution":"68566d7c4ec8e0cb02907997","page":2,"pageSize":10,"sortAscending":true}},"withCounts":{"summary":"Request with vault counts included","value":{"vault":"personal","includeCounts":true,"page":1,"pageSize":25}}}}}},"responses":{"200":{"description":"Successfully retrieved user uploads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadSearchResponse"}}}},"400":{"description":"Bad request - query failed or invalid uploadId","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Failed to get uploads <error message>"},"error":{"type":"object","description":"Full error object from the caught exception"}}}}}},"401":{"description":"Unauthorized - user not found for the provided token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - standard user attempted instance/account vault, or admin lacks `files.list` entitlement.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Standard users cannot access instance vault"}}}}}}},"tags":["IP Vault"]}},"/api/user/uploads/vault-health":{"post":{"summary":"Scan vault uploads for content quality","description":"Analyzes embedding chunks for unoptimized content (JavaScript, HTML, CSS, SQL, JSON) and assigns a vaultHealthScore (0-100) to each unscanned upload. Only processes uploads with vaultHealthScore null. Idempotent - re-running skips already-scanned files.","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vault"],"properties":{"vault":{"type":"string","enum":["personal","instance","account"],"description":"Which vault to scan"}}},"examples":{"scanPersonal":{"summary":"Scan personal vault","value":{"vault":"personal"}}}}}},"responses":{"200":{"description":"Scan completed","content":{"application/json":{"schema":{"type":"object","properties":{"scanned":{"type":"integer","description":"Number of uploads scanned"},"flagged":{"type":"integer","description":"Number of uploads with vaultHealthScore >= 60"}}}}}},"400":{"description":"Bad request - missing or invalid vault parameter, or scan failed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"vault parameter required (personal, instance, or account)"},"error":{"type":"object","description":"Full error object (only present when the catch block is reached)"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"503":{"description":"Service unavailable - the embeddings database is still connecting; retry shortly.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Embeddings database is still connecting"}}}}}}}}},"/api/user/uploads/vault-health-summary":{"post":{"summary":"Aggregate vault-wide health stats","description":"Returns per-category counts across the entire vault (Personal / Instance / Account) — not scoped to the current collection or page. Used by the Files panel's Vault Health header to compute the A-F grade and the attention dot. Computed via parallel `$match + $count` pipelines (DocumentDB-safe; no `$facet`).","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vault"],"properties":{"vault":{"type":"string","enum":["personal","instance","account"],"description":"Vault scope to summarise"},"institution":{"type":"string","description":"Optional institution _id, only honored on the `instance` vault for cross-instance admin views."}}}}}},"responses":{"200":{"description":"Summary returned","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"summary":{"type":"object","properties":{"totalCount":{"type":"integer","description":"Non-deleted uploads in scope"},"activeCount":{"type":"integer","description":"status === 'selected' (Included in RAG)"},"errorCount":{"type":"integer","description":"status === 'error'"},"neverUsedCount":{"type":"integer","description":"ragHitCount === 0 AND created > 7d ago"},"staleCount":{"type":"integer","description":"lastRagHitAt older than 90 days"},"unscannedCount":{"type":"integer","description":"vaultHealthScore not yet computed"},"unoptimizedCount":{"type":"integer","description":"vaultHealthScore >= 60 (noisy content / code-heavy)"},"unindexedCount":{"type":"integer","description":"Included but ingestion.counts.total === 0 (needs reprocess)"},"missingTerminalCount":{"type":"integer","description":"Source file missing on last access AND no chunks — unrecoverable, only valid action is Delete"},"staleBaseUrlCount":{"type":"integer","description":"Absolute file_url whose host doesn't match the current request origin (env switch)"}}},"grade":{"type":"object","description":"Composite vault-health grade derived from the summary counts. Lets API\nconsumers (UI, dashboards) display a single A–F letter + 0–100 score\nwithout re-implementing the rules client-side.\n\nSeverity weights per category (penalty per 100% of total):\n  - Critical (50pts): errorCount, unindexedCount, missingTerminalCount\n  - Degraded (25pts): unoptimizedCount, staleBaseUrlCount\n  - Stale     (10pts): neverUsedCount, staleCount\n  - Unknown    (5pts): unscannedCount\n\n`score = 100 − Σ (weight × count / totalCount)`, clamped to ≥0.\nLetter: 90+ A, 80+ B, 70+ C, 60+ D, else F. Empty vault returns\n`{ letter: 'N/A', score: null, factors: [] }`.\n","properties":{"letter":{"type":"string","enum":["A","B","C","D","F","N/A"],"example":"B"},"score":{"type":"number","nullable":true,"description":"0–100 (1-decimal), or null for empty vault","example":84.6},"factors":{"type":"array","description":"Non-zero contributors, sorted by impact desc. UI can render 'Why B+: ...' breakdown.","items":{"type":"object","properties":{"key":{"type":"string","example":"unindexedCount"},"count":{"type":"integer","example":12},"impact":{"type":"number","description":"Points deducted from 100 (1-decimal)","example":5.4},"label":{"type":"string","example":"Included but no segments indexed"}}}}}}}}}}},"400":{"description":"Bad request — missing or invalid vault parameter"},"401":{"description":"Unauthorized"}}}},"/api/user/uploads/files-with-issues":{"post":{"summary":"List every file with a vault-health issue","description":"Companion to `/api/user/uploads/vault-health-summary` — returns the full *list* of problematic files in scope (vault, or vault + collection subtree), classified into one of eight issue categories. Both endpoints share the same `$match` predicates so the list and the pill counts on the Vault Health panel always agree.\n\nIssue classification mirrors the client's legacy `filesWithIssues` priority order (first match wins):\n`missing_terminal > missing_file > unindexed > stale_base_url > unoptimized > error > never_used > stale`.\n\nReturned uncapped — problem inventory is naturally bounded by the vault's issue count, not browse pagination. Files-with-issues counts in the summary endpoint are also uncapped, so the list always matches.\n","tags":["IP Vault"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vault"],"properties":{"vault":{"type":"string","enum":["personal","instance","account"],"description":"Vault scope. Same enum as vault-health-summary."},"institution":{"type":"string","description":"Optional institution _id, honored only on the `instance` vault for cross-instance admin views."},"collection":{"type":"string","description":"Optional ObjectId. When set, narrows the listing to files inside that collection AND its descendants (walks `collection.parent` to bounded depth-10). When omitted, the listing covers the entire vault. The UI's \"List across all collections in this vault\" checkbox omits this field; unchecked + active collection passes the active collection id so the panel scopes to what the user is browsing.\n"}}}}}},"responses":{"200":{"description":"Files with issues","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"files":{"type":"array","items":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string","description":"Upload's originalname"},"issue":{"type":"string","enum":["missing_terminal","missing_file","unindexed","stale_base_url","unoptimized","error","never_used","stale"]},"score":{"type":"integer","description":"Only present for issue=unoptimized — the vaultHealthScore (0-100 percentage of code/markup/structured content)."},"hasFix":{"type":"boolean","description":"Only present for issue=stale_base_url. Currently always true on this endpoint."},"collection_path":{"type":"array","description":"Breadcrumb from root → leaf of the file's containing collection. Omitted when the file lives at the vault root.","items":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string","nullable":true}}}}}}}}}}}},"400":{"description":"Bad request — missing/invalid vault, or invalid collection id"},"401":{"description":"Unauthorized"}}}},"/api/user/upload/{id}":{"put":{"summary":"Update an existing upload file's metadata","description":"Updates the metadata of an uploaded file. Only whitelisted fields are accepted (originalname, file_title, file_summary, file_authors, file_url, source_url, status, is_private, is_public, institution, account_shared, collection, embeddings_model, summary_model, image_analysis_model, audio_analysis_model). Setting is_public to true automatically forces is_private to false. Sending institution or collection as empty string or null triggers $unset. Users with accountType user can only update their own files. Admins can update files within their own institution, or files they originally uploaded (owner match) regardless of institution.","tags":["IP Vault"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"The unique identifier of the upload to update (must be a valid ObjectId)","example":"68566d7c4ec8e0cb02907997"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUploadRequest"}}}},"responses":{"200":{"description":"Upload successfully updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUploadResponse"}}}},"400":{"description":"Bad request - invalid upload ID, empty body, or update operation failed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid Upload id"},"error":{"type":"object","description":"Full error object (only present when the catch block is reached)"}}}}}},"401":{"description":"Unauthorized - user not found for the provided token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Authentication Required"}}}}}},"403":{"description":"Forbidden - user does not own the file; standard user attempted to move a file to an instance or account vault; admin's institution does not match the file's institution and admin is not the original uploader; or admin lacks files.edit entitlement on the file's institution","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Cant update or delete a file that is not yours!"}}}}}},"404":{"description":"Upload not found","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Upload not found!"}}}}}}},"security":[{"apiKeyAuth":[]}]}},"/api/user/agents/workspace/ticket":{"post":{"summary":"Issue a short-lived ticket for the caller's KasmVNC desktop","description":"Mints a single-use, 60-second JWT ticket the browser uses to open a\nWebSocket to the in-band desktop proxy at\n`/api/agents/workspace/vnc/<workspaceId>`. The endpoint enforces:\n\n- Sec-Fetch-Site: same-origin only — cross-site invocation is rejected.\n- Per-user rate limiting via `createWorkspaceTicketLimiter()` (keyed on `req.user._id`; IP only as fallback for unauthenticated requests).\n- Ownership: the workspace must belong to the caller and be in `status: 'ready'`.\n\nThe returned `vncPassword` is the RFB-protocol password KasmVNC requires\nduring the WebSocket handshake (separate from the HTTP Basic auth the\nproxy applies server-side). Both the ticket and the password are\nshort-lived; obtain a fresh pair for each desktop session.\n\nThis route is not super-only — it is mounted under\n`/api/agents/workspace` ahead of the super gate that fronts the rest\nof `/api/agents/*`.\n","tags":["Agents"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentWorkspaceTicketRequest"}}}},"responses":{"200":{"description":"Ticket issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentWorkspaceTicketResponse"}}}},"400":{"description":"workspaceId missing or empty.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"workspaceId is required"}}}}}},"403":{"description":"Cross-site request blocked by Sec-Fetch-Site enforcement."},"404":{"description":"No ready workspace owned by the caller matches `workspaceId`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Workspace not ready"}}}}}},"429":{"description":"Rate limit exceeded — too many ticket requests for this IP."},"500":{"description":"Internal server error issuing the ticket."}}}},"/api/user/agents/workspace/me":{"get":{"summary":"Get current user's agent workspace status","description":"Returns the agent desired state from the user's active instance plus the per-user workspace read model. This route requires normal authentication, not global super access.","tags":["Agents"],"security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Workspace status returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentWorkspaceMeResponse"}}}},"404":{"description":"No active instance found"},"500":{"description":"Internal server error"}}}},"/api/ai/rtProxy/rtSession":{"post":{"summary":"Create realtime AI session","description":"Creates a new realtime AI session for voice/WebRTC communication.\nProvider, model, and voice are resolved server-side from the user's\ninstitution `rtModel` setting (OpenAI / Google Gemini Live / xAI /\nElevenLabs / LemonSlice). Returns either an ephemeral WebRTC key\n(OpenAI / Gemini / xAI), a signed-agent configuration (ElevenLabs),\nor a Daily room URL + token (LemonSlice).\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTSessionRequest"}}}},"responses":{"200":{"description":"Realtime session created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTSessionResponse"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Realtime not enabled for this institution"}}}},"/api/ai/rtTools/exec":{"post":{"summary":"Execute realtime tool","description":"Executes a tool/function during a realtime AI session.\nUsed for RAG search, web search, and other AI tool capabilities.\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTToolExecRequest"}}}},"responses":{"200":{"description":"Tool executed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTToolExecResponse"}}}},"400":{"description":"Bad request - invalid tool or arguments"},"401":{"description":"Unauthorized"}}}},"/api/ai/rtProxy/lemonslice/chat":{"post":{"summary":"Stream a LemonSlice avatar voice turn","description":"Drives a single LLM turn for the LemonSlice Hosted Pipeline avatar. The\nbackend resolves the user's institution context (assistant, RAG files,\nconversation history, memory, tools) and streams the LLM response back\nas NDJSON — one JSON object per line:\n\n- `{\"type\":\"segment\",\"kind\":\"speak\",\"text\":\"...\"}` — narration chunks\n  flushed at tool-call boundaries so the avatar can speak while a tool\n  executes. A tool-less turn emits one segment; N tools emit N+1.\n- `{\"type\":\"end\",\"text\":\"...\",\"usage\":N,\"cached\":N,\"completion\":N,\"tools\":[...]}` —\n  terminal line with the full text and token/tool accounting.\n\nResponse is `application/x-ndjson; charset=utf-8` with `X-Accel-Buffering: no`\nso HAProxy / nginx do not buffer the per-line frames.\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LemonsliceChatRequest"}}}},"responses":{"200":{"description":"NDJSON stream — one JSON object per line. Begins with zero or more\n`segment` lines and terminates with exactly one `end` line.\n","content":{"application/x-ndjson":{"schema":{"oneOf":[{"$ref":"#/components/schemas/LemonsliceChatSegmentLine"},{"$ref":"#/components/schemas/LemonsliceChatEndLine"}]}}}},"400":{"description":"Empty input — neither `input` nor `message` supplied.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","example":"Input text required"},"code":{"type":"string","example":"empty_input"}}}}}}}},"401":{"description":"Unauthorized — token missing/invalid or user not resolvable."},"500":{"description":"LemonSlice chat failed — upstream LLM or context build error.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string","example":"lemonslice_chat_error"}}}}}}}}}}},"/api/ai/rtProxy/voiceTurn":{"post":{"summary":"Stream a provider-neutral avatar voice turn (Anam + future providers)","description":"Provider-neutral alias for the per-turn voice bridge. Reuses the same\n`streamVoiceTurn` NDJSON handler as `/api/ai/rtProxy/lemonslice/chat`.\nUsed by the Anam Pria-direct Custom LLM (CUSTOMER_CLIENT_V1) frontend to\nfetch each assistant turn and stream the text into the Anam talk stream.\nPria remains the LLM/tools/RAG/IP Vault authority for every turn.\n\nResponse is `application/x-ndjson` with `segment` lines followed by one\nterminal `end` line (identical contract to the LemonSlice bridge).\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LemonsliceChatRequest"}}}},"responses":{"200":{"description":"NDJSON stream of `segment` lines terminated by one `end` line.","content":{"application/x-ndjson":{"schema":{"oneOf":[{"$ref":"#/components/schemas/LemonsliceChatSegmentLine"},{"$ref":"#/components/schemas/LemonsliceChatEndLine"}]}}}},"400":{"description":"Empty input — neither `input` nor `message` supplied."},"401":{"description":"Unauthorized — token missing/invalid or user not resolvable."},"500":{"description":"Voice turn failed — upstream LLM or context build error."}}}},"/api/ai/rtProxy/deepgram/sttSession":{"post":{"summary":"Mint a Deepgram streaming STT session","description":"Mints a short-lived Deepgram bearer token (`/v1/auth/grant`) plus the\nWSS URL the browser should open. The long-lived account API key never\nleaves the backend — the browser passes `access_token` via the WS\nsubprotocol and streams 16 kHz mono linear16 PCM directly to Deepgram.\n\nThe endpoint also resolves keyterm hints from `requestArgs`\n(institution name/ainame are added automatically) so the streaming\nSTT is biased toward the conversation's terminology.\n\nDev fallback: when `DEEPGRAM_DEV_RAW_KEY=true`, the raw account key is\nreturned directly (visible in the browser) and `expires_in=0`. Never\nenable that mode in production.\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeepgramSttSessionRequest"}}}},"responses":{"200":{"description":"STT session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeepgramSttSessionResponse"}}}},"400":{"description":"Token grant failed — bad request / missing creds / Deepgram\nupstream error. The Deepgram error status code is propagated when\navailable (e.g. 401 from Deepgram becomes 401 here).\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"}}}}}}}},"401":{"description":"Unauthorized — caller token missing/invalid or user not resolvable."}}}},"/api/ai/rtSave/conversation":{"post":{"summary":"Save realtime conversation","description":"Saves a realtime conversation to history after the session ends.\nRecords inputs, outputs, tool usage, and token consumption.\n","tags":["Realtime"],"security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTSaveConversationRequest"}}}},"responses":{"200":{"description":"Conversation saved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RTSaveConversationResponse"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"}}}},"/api/ai/personal/qanda":{"post":{"summary":"Send a message to the AI assistant","description":"Processes conversational AI requests with full context awareness including:\n- User location and timezone\n- Conversation history (via selectedCourse)\n- Assistant personality and instructions\n- RAG (Retrieval-Augmented Generation) from uploaded documents\n\n**Streaming:** For real-time response streaming, include a valid `socketId` in requestArgs.\nThe AI will stream chunks via Socket.IO to the `RECEIVE_STREAM` event.\n\n**Without Streaming:** If socketId is omitted, the full response is returned synchronously.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandARequest"},"example":{"id":1750660464754,"requestArgs":{"userISODate":"2025-06-23T06:34:24.754Z","userTimezone":"America/New_York","socketId":"DhXE7OVjCtfUmDFTAAAB","institutionPublicId":"f831501f-b645-481a-9cbb-331509aaf8c1","assistantId":"6856fa89cbafcff8d98680f5","selectedCourse":{"course_id":1750532703472,"course_name":"Research Discussion","assistant":{"_id":"6856fa89cbafcff8d98680f5"}},"ragOnly":false,"ragIgnore":false},"inputs":["What is machine learning?"],"outputs":[]}}}},"responses":{"200":{"description":"AI response generated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandAResponse"},"example":{"success":true,"streamingFailed":false,"outputs":["Machine learning is a subset of artificial intelligence..."],"usage":1234,"credits":5,"query_duration_ms":2500,"model":"gpt-4o"}}}},"400":{"description":"Invalid request - missing inputs or malformed request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required - missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error during AI processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Conversation"]}},"/api/ai/personal-stream/qanda-stream":{"post":{"summary":"Send a message with HTTP SSE streaming response","description":"Alternative to Socket.IO streaming - uses standard HTTP Server-Sent Events (SSE).\nIdeal for server-side integrations, SDKs, and environments where WebSockets aren't available.\n\n## Response Format\nReturns `text/event-stream; charset=utf-8` with SSE-formatted JSON chunks.\nEach line is prefixed with `data: ` followed by a JSON object and two newlines.\n\n```\ndata: {\"type\":\"connected\",\"message\":\"Stream connected\"}\n\ndata: {\"type\":\"stream\",\"prompt\":\"The capital\",\"delta\":\"The capital\"}\n\ndata: {\"type\":\"stream\",\"prompt\":\"The capital of France is Paris.\",\"delta\":\" of France is Paris.\"}\n\ndata: {\"type\":\"tool_call\",\"call_id\":\"tooluse_abc123\",\"name\":\"search_uploads\",\"arguments\":null,\"displayInfo\":{\"icon\":\"search\",\"label\":\"Searching documents\"}}\n\ndata: {\"type\":\"tool_result\",\"call_id\":\"tooluse_abc123\",\"name\":\"search_uploads\",\"arguments\":{\"query\":\"France capital\"},\"response\":\"...\",\"responseLength\":512,\"responseDurationMs\":150,\"success\":true}\n\ndata: {\"type\":\"complete\",\"success\":true,\"usage\":1234,\"outputs\":[\"The capital of France is Paris.\"],\"model\":\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\",\"cached\":0,\"completion\":42}\n\ndata: {\"type\":\"done\"}\n```\n\n## Event Types\n| Type | Description | Key Fields |\n|------|-------------|------------|\n| `connected` | Stream established | `message` |\n| `stream` | Text chunk from AI | `prompt` (cumulative), `delta` (incremental) |\n| `tool_call` | Tool invocation started | `call_id`, `name`, `arguments`, `displayInfo` |\n| `tool_result` | Tool execution completed | `call_id`, `name`, `response`, `success`, `responseDurationMs` |\n| `complete` | Final response with metrics | `success`, `usage`, `outputs`, `model`, `cached`, `completion` |\n| `error` | Processing error | `error.message`, `error.status` |\n| `done` | Stream ended | _(none)_ |\n\n## Response Headers\n```\nContent-Type: text/event-stream; charset=utf-8\nCache-Control: no-cache, no-transform\nConnection: keep-alive\nContent-Encoding: none\nTransfer-Encoding: chunked\nX-Accel-Buffering: no\n```\n\n## Cancellation\nClose the HTTP connection to cancel the request. The server will detect the\ndisconnect and abort any in-progress AI generation.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandAStreamRequest"},"example":{"inputs":["What is machine learning?"],"requestArgs":{"institutionPublicId":"f831501f-b645-481a-9cbb-331509aaf8c1","assistantId":"6856fa89cbafcff8d98680f5","selectedCourse":{"course_id":1750532703472,"course_name":"AI Fundamentals"},"ragOnly":false,"ragIgnore":false,"userTimezone":"America/New_York"}}}}},"responses":{"200":{"description":"SSE stream of AI response chunks","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SSEStreamEvent"},"example":"data: {\"type\":\"connected\",\"message\":\"Stream connected\"}\n\ndata: {\"type\":\"stream\",\"prompt\":\"Machine\",\"delta\":\"Machine\"}\n\ndata: {\"type\":\"stream\",\"prompt\":\"Machine learning is\",\"delta\":\" learning is\"}\n\ndata: {\"type\":\"stream\",\"prompt\":\"Machine learning is a branch of AI...\",\"delta\":\" a branch of AI...\"}\n\ndata: {\"type\":\"complete\",\"success\":true,\"usage\":1234,\"outputs\":[\"Machine learning is a branch of AI...\"],\"model\":\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\",\"cached\":0,\"completion\":42}\n\ndata: {\"type\":\"done\"}\n"}}},"400":{"description":"Invalid request (SSE error event)","content":{"text/event-stream":{"example":"data: {\"type\":\"error\",\"error\":{\"message\":\"Input text required\",\"status\":400}}\ndata: {\"type\":\"done\"}\n"}}},"401":{"description":"Authentication required (SSE error event)","content":{"text/event-stream":{"example":"data: {\"type\":\"error\",\"error\":{\"message\":\"Authentication Required\",\"status\":401}}\ndata: {\"type\":\"done\"}\n"}}},"500":{"description":"Server error (SSE error event)","content":{"text/event-stream":{"example":"data: {\"type\":\"error\",\"error\":{\"message\":\"Internal server error\",\"status\":500}}\ndata: {\"type\":\"done\"}\n"}}}},"tags":["Conversation"]}},"/api/ai/experimental/personal/qanda":{"post":{"summary":"Send a message to the AI assistant (experimental — Soul Document prompt)","description":"Experimental variant of `/api/ai/personal/qanda`. Identical request /\nresponse contract and Socket.IO streaming semantics, but the system\nprompt is generated by the **Soul Document V1** prompt generator\n(`getSoulDocumentPrompt` / `getSoulDocumentPromptBypassed`) instead of\nthe production fragmented-prompt generator. Used for A/B testing\nprompt architectures against the classic production endpoint.\n\nBehavior parity with `/api/ai/personal/qanda`:\n- Same `inputs[]` + `requestArgs` request shape.\n- Same Socket.IO streaming (provide `requestArgs.socketId` for\n  `RECEIVE_STREAM` chunks; omit for synchronous response).\n- Same downstream middleware chain (`creditCheck`,\n  `contentFilterCheck`, `creditPayment`, `saveToHistory`,\n  `sendResponse`).\n\nDifferences vs. `/api/ai/personal/qanda`:\n- `req.locals.experimental = true` and `req.locals.promptMode = 'soul'`\n  are stamped on the history record for downstream comparison.\n- Error emails are sent with an `EXPERIMENTAL ENDPOINT` label so they\n    can be filtered out of production triage.\n- The handler re-loads the user via its own `User.findOne({_id})`\n  and populates `institution` + `institution.account` directly,\n  instead of consuming `req.locals.resolvedUser` produced upstream.\n  The `resolveInstitution` middleware still runs (so\n  `institutionPublicId` is validated and the institution switch is\n  persisted), but the user object the handler operates on is fresh\n  from this re-load — matching the soul-comparison harness's\n  expectations.\n\nSee `docs/plans/2026-02-01-soul-document-experiment-design.md` and\n`test/soul-comparison/runner.js` for the A/B harness that\ndrives this endpoint.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandARequest"},"example":{"id":1750660464754,"requestArgs":{"userISODate":"2025-06-23T06:34:24.754Z","userTimezone":"America/New_York","socketId":"DhXE7OVjCtfUmDFTAAAB","assistantId":"6856fa89cbafcff8d98680f5","selectedCourse":{"course_id":1750532703472,"course_name":"Research Discussion","assistant":{"_id":"6856fa89cbafcff8d98680f5"}},"ragOnly":false,"ragIgnore":false},"inputs":["What is machine learning?"],"outputs":[]}}}},"responses":{"200":{"description":"AI response generated successfully (Soul Document V1 system prompt)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandAResponse"},"example":{"success":true,"streamingFailed":false,"outputs":["Machine learning is a subset of artificial intelligence..."],"usage":1234,"credits":5,"query_duration_ms":2500,"model":"gpt-4o"}}}},"400":{"description":"Invalid request — missing `inputs` array, empty prompt after join+trim, or missing body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required — missing `req.user._id` or user not found in database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error during AI processing (error captured in history and emailed with EXPERIMENTAL label)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Conversation"]}},"/api/ai/experimental/personal/qanda-v2":{"post":{"summary":"Send a message to the AI assistant (experimental — Soul Document V2 prompt)","description":"Second-generation experimental variant of `/api/ai/personal/qanda`.\nIdentical request / response contract to the V1 experimental endpoint,\nbut uses the **Soul Document V2** prompt generator\n(`getSoulDocumentPromptV2` / `getSoulDocumentPromptV2Bypassed`) which\nrefines the cohesive-anchor architecture introduced in V1. Used for\nthree-way A/B testing alongside classic `/api/ai/personal/qanda` and\nthe V1 experimental endpoint.\n\nBehavior is otherwise identical to `/api/ai/experimental/personal/qanda`:\n- Same `inputs[]` + `requestArgs` request shape (`QandARequest`).\n- Same Socket.IO streaming via optional `requestArgs.socketId`.\n- Same downstream middleware chain (`creditCheck`,\n  `contentFilterCheck`, `creditPayment`, `saveToHistory`,\n  `sendResponse`).\n- Same handler-level `User.findOne({_id})` re-load (the\n  `resolveInstitution` middleware still runs upstream, but the\n  handler operates on the freshly re-loaded user object).\n- `req.locals.experimental = true` and `req.locals.promptMode = 'soul-v2'`\n  are stamped on the history record so the A/B harness can distinguish\n  V1 from V2 runs.\n\nSee `docs/plans/2026-02-01-soul-document-experiment-design.md` and the\nthree-way comparison runner in `test/soul-comparison/`.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandARequest"},"example":{"id":1750660464754,"requestArgs":{"userISODate":"2025-06-23T06:34:24.754Z","userTimezone":"America/New_York","socketId":"DhXE7OVjCtfUmDFTAAAB","assistantId":"6856fa89cbafcff8d98680f5","selectedCourse":{"course_id":1750532703472,"course_name":"Research Discussion","assistant":{"_id":"6856fa89cbafcff8d98680f5"}},"ragOnly":false,"ragIgnore":false},"inputs":["What is machine learning?"],"outputs":[]}}}},"responses":{"200":{"description":"AI response generated successfully (Soul Document V2 system prompt)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QandAResponse"},"example":{"success":true,"streamingFailed":false,"outputs":["Machine learning is a subset of artificial intelligence..."],"usage":1234,"credits":5,"query_duration_ms":2500,"model":"gpt-4o"}}}},"400":{"description":"Invalid request — missing `inputs` array, empty prompt after join+trim, or missing body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required — missing `req.user._id` or user not found in database","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error during AI processing (error captured in history and emailed with EXPERIMENTAL label)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Conversation"]}},"/api/ai/chat/completions":{"post":{"tags":["AI"],"summary":"OpenAI-compatible Chat Completions endpoint (inbound integration)","description":"OpenAI-compatible streaming chat completions endpoint. Accepts a `messages[]`\narray, extracts the last user message as the active turn, replays any prior\nuser/assistant messages as conversation history (sanitized for Bedrock's\nalternating-role requirement), runs the active turn through Praxis's\nRAG/tool pipeline, and streams back OpenAI-format SSE chunks.\n\n**Today's primary consumer:** the ElevenLabs Voice Agent in Convo (Direct)\nmode — its custom-LLM webhook points at this endpoint.\n\n**OpenAI compatibility surface (what Pria reads vs. ignores):**\nPria accepts the OpenAI shape but only reads `messages[]` and `model`.\n`model` is informational — the effective model is determined by the\nPraxis cascade (assistant > `chatCompletionModel` > institution\nconversationModel). Fields commonly seen on OpenAI clients but\n**silently ignored** here: `tools`, `tool_choice`, `temperature`,\n`max_tokens`, `stream`, `stream_options`, `top_p`, `n`,\n`frequency_penalty`, `presence_penalty`, `response_format`, `seed`,\n`logit_bias`, `user`. The response is always SSE (server forces\nstreaming mode regardless of `stream`). Tool calls are executed\nserver-side by Pria's tool runtime — they are not surfaced as OpenAI\n`tool_calls` deltas; tool acknowledgements appear inline as spoken\nphrases in the `content` stream.\n\n**Per-institution gate.** Disabled by default. The administrator must\nset `institution.chatCompletionEnabled = true` to allow inbound traffic.\nDisabled institutions receive `403 chat_completion_disabled`.\n\n**Override fields** (institution-level, all optional):\n- `chatCompletionModel` — overrides the conversation model and provider\n  routing for inbound requests. Priority: `assistant.conversationModel`\n  > `chatCompletionModel` > `institution.conversationModel`. Assistant\n  always wins — the override only applies when no assistant has\n  overridden the conversation model. Empty/unset = inherit from\n  existing cascade.\n- `chatCompletionMaxCompletionTokens` — overrides `maxCompletionTokens`.\n  Sentinel: `-1` = inherit, `0` = Auto (catalog cap), `>0` = explicit.\n- `chatCompletionReasoningEffort` — overrides `reasoningEffort`. Empty\n  string = inherit. Common voice-mode value: `'none'`.\n\nDetection of \"this is a chat-completion inbound request\" is path-based —\nany request landing here sets `requestArgs.isChatCompletion = true`,\nwhich the override helpers in `rag.js` and `reasoning_effort_utils.js`\nread to apply the cascade above.\n","parameters":[{"in":"header","name":"x-access-token","schema":{"type":"string"},"required":true,"description":"Praxis JWT"},{"in":"header","name":"x-praxis-institution-public-id","schema":{"type":"string"},"required":false,"description":"Public ID of the institution context (optional, defaults to user's primary institution)"},{"in":"header","name":"x-praxis-conversation-id","schema":{"type":"string"},"required":false,"description":"Conversation/course ID"},{"in":"header","name":"x-praxis-assistant-id","schema":{"type":"string"},"required":false,"description":"Assistant ObjectId (24-char hex)"},{"in":"header","name":"x-praxis-timezone","schema":{"type":"string"},"required":false,"description":"IANA timezone string (e.g. \"America/New_York\") for date-aware prompts"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","description":"OpenAI-format messages array. The last `user` message is the\nactive turn; earlier `user` and `assistant` entries are\nreplayed as conversation history. `system` and `tool`\nmessages are accepted in the shape but ignored — Pria\nbuilds its own system prompt from assistant + institution\nsettings, and tool execution is server-managed.\n","items":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["system","user","assistant","tool"]},"content":{"description":"OpenAI content shape. String form is used as-is.\nArray form (typed content parts) is normalized so that\n`text` and `refusal` parts are joined while\n`input_audio`, `image_url`, and `file` parts are\ndropped.\n","oneOf":[{"type":"string"},{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","image_url","input_audio","file","refusal"]},"text":{"type":"string","description":"Present when the part type is text."}}}}]},"name":{"type":"string","description":"Ignored. Accepted for OpenAI shape compatibility."},"tool_call_id":{"type":"string","description":"Ignored. Accepted for OpenAI shape compatibility."},"tool_calls":{"type":"array","description":"Ignored. Tool calls are server-managed by Pria's tool runtime.","items":{"type":"object"}}}}},"model":{"type":"string","description":"Informational only — echoed back in SSE chunks as\n`choices[].delta.model`. The actual model dispatched is\ndetermined by the Praxis cascade\n(`assistant.conversationModel` >\n`institution.chatCompletionModel` >\n`institution.conversationModel`).\n","example":"pria"},"stream":{"type":"boolean","description":"Ignored — the endpoint always returns `text/event-stream`.\nAccepted for OpenAI shape compatibility.\n","example":true}}}}}},"responses":{"200":{"description":"SSE stream of OpenAI-format completion chunks. Terminated with a final `data: [DONE]` line.","content":{"text/event-stream":{"schema":{"type":"string","example":"data: {\"id\":\"chatcmpl-1735...\",\"object\":\"chat.completion.chunk\",\"created\":1735000000,\"model\":\"pria\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null,\"logprobs\":null}]}\n\ndata: {\"id\":\"chatcmpl-1735...\",\"object\":\"chat.completion.chunk\",\"created\":1735000000,\"model\":\"pria\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null,\"logprobs\":null}]}\n\ndata: [DONE]\n"}}}},"400":{"description":"Empty / malformed request body (e.g. no user message).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"type":{"type":"string","example":"invalid_request_error"},"code":{"type":"string","example":"empty_user_message"}}}}}}}},"401":{"description":"Missing or invalid `x-access-token`."},"403":{"description":"Chat Completion endpoint is disabled for this institution. Returned\nwhen `institution.chatCompletionEnabled !== true`. Admin must opt-in\nper institution.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","example":"Chat Completion endpoint is not enabled for this institution."},"type":{"type":"string","example":"forbidden"},"code":{"type":"string","example":"chat_completion_disabled"}}}}}}}}}}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-access-token","description":"JWT token passed in x-access-token header"},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT token passed in authorization header"},"priaApiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Long-lived Pria API key (format `pria_<40 hex chars>`) used to obtain a JWT via\n`POST /api/auth/api-key-signin`. Only valid on the api-key-signin endpoint —\nevery other admin/user endpoint expects the JWT issued by that exchange (sent as\n`Authorization: Bearer <jwt>` or `x-access-token: <jwt>`).\n"}},"schemas":{"ErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","description":"Human-readable error message"},"error":{"type":"string","description":"Technical error details"}}},"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","description":"Success message"}}},"GPSCoordinates":{"type":"object","description":"Geographic location data from device","properties":{"accuracy":{"type":"number","description":"GPS accuracy in meters","example":21126.84},"latitude":{"type":"number","description":"Latitude coordinate (-90 to 90)","example":-21.282816},"longitude":{"type":"number","description":"Longitude coordinate (-180 to 180)","example":55.4139648},"altitude":{"type":"number","nullable":true,"description":"Altitude in meters above sea level"},"altitudeAccuracy":{"type":"number","nullable":true,"description":"Altitude accuracy in meters"},"heading":{"type":"number","nullable":true,"description":"Direction of travel in degrees (0-360)"},"speed":{"type":"number","nullable":true,"description":"Speed in meters per second"}}},"AssistantReference":{"type":"object","description":"Reference to an AI assistant for conversation context","properties":{"_id":{"type":"string","description":"Assistant unique identifier (MongoDB ObjectId)","example":"6856fa89cbafcff8d98680f5"},"name":{"type":"string","description":"Assistant display name","example":"Research Assistant"}}},"ConversationContext":{"type":"object","description":"Context for a conversation session (course/topic)","properties":{"course_id":{"type":"number","description":"Unique conversation/course identifier (epoch timestamp)","example":1750532703472},"course_name":{"type":"string","description":"Display name for the conversation","example":"Research Project Discussion"},"assistant":{"$ref":"#/components/schemas/AssistantReference"},"history_count":{"type":"integer","description":"Number of dialogue entries in this conversation","example":15},"last_dialogue_date":{"type":"string","format":"date-time","description":"Timestamp of most recent message"}}},"InstitutionSummary":{"type":"object","description":"Basic institution information for references","properties":{"_id":{"type":"string","description":"Institution unique identifier","example":"6631915765bb0a94cfd6ca99"},"name":{"type":"string","description":"Institution domain identifier","example":"myschool.instructure.com"},"ainame":{"type":"string","description":"AI assistant display name","example":"Hugo"},"picture":{"type":"string","format":"uri","description":"Institution avatar/logo URL"},"status":{"type":"string","enum":["active","inactive"],"description":"Institution status"}}},"UserSummary":{"type":"object","description":"Basic user information for references","properties":{"_id":{"type":"string","description":"User unique identifier"},"email":{"type":"string","format":"email","description":"User email address"},"fname":{"type":"string","description":"First name"},"lname":{"type":"string","description":"Last name"},"accountType":{"type":"string","enum":["user","admin","super"],"description":"User account privilege level"}}},"PaginationMeta":{"type":"object","description":"Pagination metadata for list responses","properties":{"total":{"type":"integer","description":"Total number of records"},"limit":{"type":"integer","description":"Maximum records per page"},"offset":{"type":"integer","description":"Number of records skipped"},"hasMore":{"type":"boolean","description":"Whether more records are available"}}},"TokenUsage":{"type":"object","description":"AI token consumption metrics","properties":{"usage":{"type":"integer","description":"Total tokens consumed","example":1234},"cached":{"type":"integer","description":"Tokens served from cache","example":500},"completion":{"type":"integer","description":"Tokens used for response generation","example":734}}},"CreditInfo":{"type":"object","description":"Credit consumption and balance information","properties":{"credits":{"type":"number","description":"Credits consumed for this request"},"creditsUsed":{"type":"number","description":"Total credits used by user/institution"},"totalCredits":{"type":"number","description":"Total credits available"}}},"DatabaseStatusResponse":{"type":"object","properties":{"message":{"type":"string","description":"Description of database connection states","example":"0: disconnected, 1: connected, 2: connecting, 3: disconnecting, 4: invalid credentials"},"status":{"type":"integer","description":"Current database connection status (0-4)","example":1,"enum":[0,1,2,3,4]}}},"MemoryInfo":{"type":"object","properties":{"rss":{"type":"number","description":"Resident Set Size - total memory allocated for the process"},"heapTotal":{"type":"number","description":"Total heap memory allocated"},"heapUsed":{"type":"number","description":"Heap memory currently in use"},"external":{"type":"number","description":"Memory used by C++ objects bound to JavaScript objects"},"arrayBuffers":{"type":"number","description":"Memory allocated for ArrayBuffers and SharedArrayBuffers"}}},"Dependencies":{"type":"object","properties":{"database":{"type":"number","description":"Database connection status (1 = connected)"}}},"EventLoopLag":{"type":"object","nullable":true,"description":"Event loop lag percentiles in ms over the current sampling window. Null until the monitor's histogram has data.","properties":{"meanMs":{"type":"number","description":"Mean event loop lag (ms)"},"p50Ms":{"type":"number","description":"P50 event loop lag (ms)"},"p95Ms":{"type":"number","description":"P95 event loop lag (ms)"},"p99Ms":{"type":"number","description":"P99 event loop lag (ms)"},"maxMs":{"type":"number","description":"Max event loop lag (ms) since last reset"}}},"MongoInfo":{"type":"object","properties":{"state":{"type":"number","description":"0=disconnected, 1=connected, 2=connecting, 3=disconnecting"},"label":{"type":"string","example":"connected"}}},"HealthResponse":{"type":"object","properties":{"status":{"type":"string","description":"Overall health status","example":"ok"},"uptime":{"type":"number","description":"Server uptime in seconds"},"timestamp":{"type":"number","description":"Current timestamp in milliseconds"},"summary":{"type":"string","description":"Human-readable one-line digest matching the heartbeat log format","example":"Event loop lag: 1.9s | RSS: 1.7GB | heap: 506MB/1.1GB | mongo: connected"},"eventLoopLag":{"$ref":"#/components/schemas/EventLoopLag"},"mongo":{"$ref":"#/components/schemas/MongoInfo"},"memory":{"$ref":"#/components/schemas/MemoryInfo"},"dependencies":{"$ref":"#/components/schemas/Dependencies"}}},"TestUrlRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The LTI context URL to be tested. Used to find an institution by matching against registered LTI context IDs.","example":"https://domain.edu/7891273"},"institutionid":{"type":"string","format":"uuid","description":"The public ID of the institution to lookup directly. Used as fallback if URL lookup fails.","example":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"},"email":{"type":"string","format":"email","description":"The email address of the user to check membership status for.","example":"user@domain.edu"},"userid":{"type":"string","description":"The LXP user ID to lookup the user by (alternative to email).","example":"lxp_12345"}}},"TestUrlResponse":{"type":"object","properties":{"ltiassigned":{"type":"boolean","description":"Indicates if the institution was found via LTI context URL match","example":true},"name":{"type":"string","description":"Name of the institution","example":"Acme University"},"status":{"type":"string","description":"Status of the institution, user, or user-institution membership. Possible values include 'active', 'inactive', 'pending', 'suspended', 'invalid'.","example":"active","enum":["active","inactive","pending","suspended","invalid","deleted"]},"ainame":{"type":"string","description":"AI/Digital Twin name associated with the institution","example":"Hugo"},"picture":{"type":"string","description":"Picture URL for the institution branding","example":"https://storage.example.com/logo.png"},"publicId":{"type":"string","format":"uuid","description":"Public identifier for the institution","example":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"}}},"AgentBrandingRequest":{"type":"object","description":"Request to retrieve institution branding for SDK/web embedding","properties":{"publicId":{"type":"string","format":"uuid","description":"The public identifier (UUID) for the digital twin instance","example":"5f03e5e6-9dde-4ddc-b686-7a77ac617e52"}}},"AgentBrandingResponse":{"type":"object","description":"Institution branding and configuration for UI customization","properties":{"picture":{"type":"string","format":"uri","description":"URL to the agent's profile picture (avatar)","example":"https://pria.praxislxp.com/logo192_v3.webp"},"picture_animated":{"type":"string","format":"uri","description":"URL to the agent's animated avatar (GIF/WebP)","example":""},"picture_bg":{"type":"string","format":"uri","description":"URL to the agent's background image (light mode)","example":"https://pria.praxislxp.com/pria_bg2.webp"},"picture_dark_bg":{"type":"string","format":"uri","description":"URL to the agent's background image (dark mode)","example":""},"elevenlabs_agent_id":{"type":"string","description":"ElevenLabs Conversational AI Agent ID for voice-enabled digital twin","example":"agent_0201kedjfdgee87bjke8acq3pcbg"},"about":{"type":"string","description":"AI agent's purpose and capabilities description","example":"I am an AI-powered virtual mentor programmed to help experts, students, faculty, and researchers with their digital corporate, education, research, and career goals..."},"ainame":{"type":"string","description":"Display name of the AI agent","example":"Pria"},"status":{"type":"string","enum":["active","inactive"],"description":"Current status of the digital twin","example":"active"}}},"SignupRequest":{"type":"object","required":["email","password","fname"],"properties":{"email":{"type":"string","format":"email","description":"User email address","example":"john.doe@domain.com"},"password":{"type":"string","format":"password","minLength":6,"description":"User password (minimum 6 characters)","example":"mySecurePassword123"},"fname":{"type":"string","description":"First name","example":"John"},"lname":{"type":"string","description":"Last name","example":"Doe"},"picture":{"type":"string","format":"uri","description":"Profile picture URL"}}},"SignupResponse":{"type":"object","properties":{"token":{"type":"string","description":"JWT authentication token"},"profile":{"$ref":"#/components/schemas/UserProfile"}}},"GenerateResetCodeRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Email address of the user","example":"john.doe@domain.com"}}},"GenerateResetCodeResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Reset code sent to email"}}},"CheckResetCodeRequest":{"type":"object","required":["email","resetCode"],"properties":{"email":{"type":"string","format":"email","description":"User email address","example":"john.doe@domain.com"},"resetCode":{"type":"string","description":"Reset code received via email","example":"ABC123"}}},"CheckResetCodeResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"valid":{"type":"boolean","description":"Whether the reset code is valid","example":true}}},"ChangePasswordRequest":{"type":"object","required":["email","resetCode","password"],"properties":{"email":{"type":"string","format":"email","description":"User email address","example":"john.doe@domain.com"},"resetCode":{"type":"string","description":"Valid reset code","example":"ABC123"},"password":{"type":"string","format":"password","minLength":6,"description":"New password (minimum 6 characters)","example":"newSecurePassword456"}}},"ChangePasswordResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Password changed successfully"}}},"CheckActivateCodeRequest":{"type":"object","required":["email","activateCode"],"properties":{"email":{"type":"string","format":"email","description":"User email address"},"activateCode":{"type":"string","description":"Account activation code"}}},"CheckActivateCodeResponse":{"type":"object","properties":{"success":{"type":"boolean"},"valid":{"type":"boolean","description":"Whether the activation code is valid"}}},"GetInstitutionsForContextRequest":{"type":"object","required":["ltiContextId"],"properties":{"ltiContextId":{"type":"string","description":"LTI context identifier","example":"https://domain.edu/course/123"}}},"GetInstitutionsForContextResponse":{"type":"object","properties":{"success":{"type":"boolean"},"institutions":{"type":"array","items":{"$ref":"#/components/schemas/InstitutionProfile"}}}},"GoogleOAuthValidateResponse":{"type":"object","properties":{"valid":{"type":"boolean","description":"Whether the stored Google access_token is currently accepted by Google.","example":true},"source":{"type":"string","nullable":true,"enum":["institution","user"],"description":"Which storage location the validated token came from. `institution` means it\nlived on the user's `UserInstitution.googleLoginToken`; `user` means the\npersonal token on the User record. Null when no token is configured.\n"},"cleared":{"type":"boolean","description":"Set to true when an invalid token was just cleared from the database.","example":false},"message":{"type":"string","description":"Set when no token is configured for the resolved source."},"error":{"type":"string","description":"Set on validation failure — passes through Google's error message."}}},"GoogleOAuthRevokeResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Google services access revoked"}}},"SignInRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"john.doe@mydomain.com"},"password":{"type":"string","format":"password","example":"iLovePria123"}}},"SignInResponse":{"type":"object","description":"Successful signin response shape. Two variants are returned by the\nsame endpoint depending on whether email MFA is required:\n  • **JWT issued** — `{ token, profile }`. The user is signed in.\n  • **MFA challenge** — `{ mfaRequired: true, challengeId, maskedEmail, mandatorySuper? }`.\n    The client must POST the 6-digit code to `/api/auth/mfa/verify`\n    with the challengeId; the verify endpoint then issues the JWT.\n  Discriminate via `mfaRequired === true` (per Phase 1 design §6.1).\n","properties":{"token":{"type":"string","description":"Signed JWT token. Present when MFA is not required or has just been verified. Include this in subsequent API requests via the x-access-token header or Authorization Bearer header. Expires after 6 hours (configurable via JWT_VALIDITY_SEC). Automatically refreshed on profile load (sliding session).","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NDMwNzM2ZmQ2MmQ2NTAwNDA0MjA2NzQiLCJlbWFpbCI6ImpvaG4uZG9lQG15ZG9tYWluLmNvbSIsImN1c3RvbWVySWQiOiJjdXNfTnh4eHh4eCIsImFjY291bnRUeXBlIjoidXNlciIsInNlc3Npb25JZCI6InMlM0FhYmMxMjMiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDA4NjQwMH0.signature"},"profile":{"$ref":"#/components/schemas/UserProfile"},"mfaRequired":{"type":"boolean","description":"When true, the response is an MFA challenge — no JWT issued. Client should redirect to the MFA verify screen with the challengeId.","example":true},"challengeId":{"type":"string","description":"MongoDB ObjectId of the issued mfaChallenge. Only present when `mfaRequired: true`. POST this to `/api/auth/mfa/verify` alongside the 6-digit code.\n","example":"6856fa89cbafcff8d98680f5"},"maskedEmail":{"type":"string","description":"Partially-masked email address the verification code was sent to (for the verify-screen \"code sent to …\" prompt). Only present when `mfaRequired: true`.\n","example":"j*****e@example.com"},"mandatorySuper":{"type":"boolean","description":"Phase 2 — when `true`, this MFA challenge was issued under\nsuper-mandatory enforcement (MFA_SUPER_MANDATORY=true and the\nuser is past the rollout date). The verify screen should\nrender an explanatory banner and suppress the Cancel\naffordance, since the user can't dismiss the flow without\nenrolling. On successful verify, the server persists\n`user.mfaEnabled = true` so the next signin follows the\nnormal phase-1 trusted-device path.\n\nOnly present when `mfaRequired: true` AND the gate fired.\nOmitted (not `false`) otherwise — clients should default to\n`false` when absent.\n","example":true}}},"AutoSignupRequest":{"type":"object","required":["email","fname","lxp_user_id","lxp_user_type","lxp_partner_id","lxp_partner_name","lxp_role_id","lxp_role_name","institution_id","institution_url","client_ip","lticontextid","digitaltwin"],"properties":{"email":{"type":"string","format":"email","example":"john.doe@praxis-ai.com"},"fname":{"type":"string","example":"John"},"lname":{"type":"string","example":"Doe"},"picture":{"type":"string","example":""},"lxp_user_id":{"type":"string","example":"1750665682"},"lxp_user_type":{"type":"integer","example":1},"lxp_partner_id":{"type":"string","example":"1"},"lxp_partner_name":{"type":"string","example":"Edu School"},"lxp_role_id":{"type":"integer","example":123},"lxp_role_name":{"type":"string","example":"Course 123"},"institution_id":{"type":"string","format":"uuid","example":"bc6efd03-9d01-43e7-bd49-c4af1c54ae3a"},"institution_url":{"type":"string","example":"https://domain.edu"},"client_ip":{"type":"string","example":"134.123.234.234"},"lticontextid":{"type":"string","example":"https://domain.edu/course/i12"},"digitaltwin":{"type":"boolean","example":true}}},"InstitutionProfile":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"picture":{"type":"string"},"picture_bg":{"type":"string"},"picture_dark_bg":{"type":"string"},"picture_animated":{"type":"string"},"elevenlabs_agent_id":{"type":"string"},"credits":{"type":"integer"},"status":{"type":"string"},"allowJoining":{"type":"string"},"joiningAdminOnly":{"type":"boolean"},"publicId":{"type":"string","format":"uuid"},"publicAuthorizedUrls":{"type":"array","items":{"type":"string"}},"ainame":{"type":"string"},"contactEmail":{"type":"string","format":"email"},"creditAward":{"type":"integer"},"poolCredits":{"type":"boolean"},"invoices_urls":{"type":"array","items":{"type":"string"}},"maxCompletionTokens":{"type":"integer"},"disableFileUploadForUser":{"type":"boolean"},"disableAudioNotesForUser":{"type":"boolean"},"toolsDisabled":{"type":"array","items":{"type":"string"}},"ltiContextIds":{"type":"array","items":{"type":"string"}},"personalisationAsked":{"type":"boolean"},"locationEnabled":{"type":"boolean"},"rtEnabled":{"type":"boolean"},"rtAdminOnly":{"type":"boolean"},"displayAgentDetails":{"type":"boolean"},"displayThinkingDetails":{"type":"boolean"},"displayRagSearchDetails":{"type":"boolean"},"displayThinkingExecution":{"type":"boolean"},"displayToolExecution":{"type":"boolean"},"assistantsDisabled":{"type":"array","items":{"type":"string"}},"disableAssistantsForUser":{"type":"boolean"},"rtVoice":{"type":"string"},"maxFiles":{"type":"integer"},"questionType":{"type":"string"},"creditsTotal":{"type":"integer","nullable":true},"creditsUsagePct":{"type":"number"},"id":{"type":"string"}}},"UserProfile":{"type":"object","properties":{"_id":{"type":"string","description":"User unique identifier"},"email":{"type":"string","format":"email","description":"User email address"},"fname":{"type":"string","description":"First name"},"lname":{"type":"string","description":"Last name"},"picture":{"type":"string","description":"Profile picture URL"},"accountType":{"type":"string","enum":["super","admin","user"],"description":"Account type"},"permissions":{"type":"array","items":{"type":"string"}},"customerId":{"type":"string"},"lxp_user_id":{"type":"string"},"lxp_user_type":{"type":"integer"},"lxp_partner_id":{"type":"string"},"lxp_partner_name":{"type":"string"},"lxp_role_id":{"type":"integer"},"lxp_role_name":{"type":"string"},"credits":{"type":"integer"},"creditsUsed":{"type":"integer"},"plan":{"type":"string","description":"Subscription plan"},"status":{"type":"string","enum":["active","inactive","deleted"],"description":"Account status"},"trial_end":{"type":"string","format":"date-time"},"trial_used":{"type":"boolean"},"current_period_end":{"type":"string","format":"date-time"},"cancel_at_period_end":{"type":"boolean"},"referralId":{"type":"string","format":"uuid"},"referrerPaid":{"type":"boolean"},"resetCodeId":{"type":"string","format":"uuid"},"invoices_urls":{"type":"array","items":{"type":"string"}},"remember_history_count":{"type":"integer","description":"Number of history items to remember"},"browser_voice":{"type":"string"},"rt_voice":{"type":"string"},"use_location":{"type":"boolean","description":"Whether to use location services"},"showSideBar":{"type":"boolean"},"dark_mode":{"type":"boolean","description":"Whether dark mode is enabled"},"created":{"type":"string","format":"date-time"},"__v":{"type":"integer"},"institution":{"$ref":"#/components/schemas/InstitutionProfile","type":"object","description":"Current institution data"},"use_stt":{"type":"boolean","description":"Whether speech-to-text is enabled"},"mustChangePassword":{"type":"boolean","description":"Whether user must change password"},"updatePasswordOnSSO":{"type":"boolean","description":"Reset password on SSO login"},"mfaEnabled":{"type":"boolean","description":"Whether email-based MFA is enabled for this account. When true, signin from a new device/IP requires a 6-digit code emailed to the user."},"mfaEnabledAt":{"type":"string","format":"date-time","description":"Timestamp when MFA was first enabled. Null when mfaEnabled is false."},"mfaPromptDismissedAt":{"type":"string","format":"date-time","description":"When the user last clicked \"Remind me later\" on the MFA opt-in nudge. The post-login prompt re-appears 7 days after this timestamp."}}},"AutoSignupResponse":{"type":"object","properties":{"token":{"type":"string","description":"JWT authentication token"},"profile":{"$ref":"#/components/schemas/UserProfile"}}},"CreateInstitutionRequest":{"type":"object","required":["name","contactEmail"],"properties":{"name":{"type":"string","description":"Institution name identifier","example":"teacher1.domain.edu.instructure"},"contactEmail":{"type":"string","format":"email","description":"Contact email for the institution","example":"jane.doe@praxis-ai.com"},"ltiContextIds":{"type":"array","items":{"type":"string"},"description":"Array of LTI context identifiers (defaults to empty array if omitted)","example":["https://domain.edu/7891278"]},"publicAuthorizedUrls":{"type":"array","items":{"type":"string"},"description":"Array of authorized public URLs","example":["https://domain.edu"]},"ainame":{"type":"string","description":"AI assistant name","example":"Hugo"},"picture":{"type":"string","format":"uri","description":"URL to the institution's picture/avatar","example":"https://hiimpria.ai/uploads/6430736fd62d650040420674/Clarkey20Headshot20Animated_1752713782148.gif"},"picture_bg":{"type":"string","description":"Background picture URL","example":""},"picture_dark_bg":{"type":"string","description":"Dark mode background picture URL","example":""},"picture_animated":{"type":"string","format":"uri","description":"URL to animated picture/avatar","example":"https://hiimpria.ai/uploads/6430736fd62d650040420674/Clarkey20Headshot20Animated_1752713782148.gif"},"elevenlabs_agent_id":{"type":"string","description":"ElevenLabs Agent ID for Digital Twin Voice","example":"agent_xxxxxxxxxxxxxxxxxxxx"},"prompt":{"type":"string","description":"Custom prompt/instructions for the AI persona","example":""},"domain":{"type":"string","description":"Institution domain, used for account lookup","example":"domain.edu"},"account":{"type":"string","description":"Parent account ObjectId (if known)","example":"6430736fd62d650040420674"},"allowJoining":{"type":"string","enum":["disabled","account","public"],"description":"Who can join this Digital Twin","example":"disabled"},"poolCredits":{"type":"boolean","description":"Whether credits are pooled on behalf of users","example":true},"creditAward":{"type":"integer","description":"Credits awarded to new users on registration (when poolCredits is false)","example":0},"questionType":{"type":"string","description":"Type of personalization question bank (CORPORATE for Digital Twin, INSTITUTION for Digital Expert)","example":"CORPORATE"},"rtEnabled":{"type":"boolean","description":"Enable real-time voice conversations","example":false},"rtAdminOnly":{"type":"boolean","description":"Restrict real-time conversations to admins only","example":true}}},"CreateInstitutionResponse":{"type":"object","properties":{"_id":{"type":"string","description":"Unique institution identifier","example":"68793ef2a8a4a5eaff36e7ca"},"name":{"type":"string","example":"teacher1.domain.edu.instructure"},"picture":{"type":"string","format":"uri","example":"https://hiimpria.ai/uploads/6430736fd62d650040420674/Clarkey20Headshot20Animated_1752713782148.gif"},"picture_bg":{"type":"string","example":""},"picture_dark_bg":{"type":"string","example":""},"picture_animated":{"type":"string","format":"uri","example":"https://hiimpria.ai/uploads/6430736fd62d650040420674/Clarkey20Headshot20Animated_1752713782148.gif"},"elevenlabs_agent_id":{"type":"string","description":"ElevenLabs Agent ID for Digital Twin Voice","example":"agent_xxxxxxxxxxxxxxxxxxxx"},"credits":{"type":"number","description":"Total credits allocated","example":50},"status":{"type":"string","enum":["active","inactive"],"example":"active"},"allowJoining":{"type":"string","enum":["disabled","account","public"],"example":"disabled"},"joiningAdminOnly":{"type":"boolean","example":false},"publicId":{"type":"string","format":"uuid","description":"Public identifier for the institution","example":"2e1006ec-5b59-4431-96d2-b0e1b1022a3a"},"publicAuthorizedUrls":{"type":"array","items":{"type":"string"},"example":["https://domain.edu"]},"ainame":{"type":"string","example":"Hugo"},"prompt":{"type":"string","example":""},"contactEmail":{"type":"string","format":"email","example":"jane.doe@praxis-ai.com"},"creditAward":{"type":"number","example":0},"poolCredits":{"type":"boolean","example":true},"questionType":{"type":"string","description":"Personalization question bank type","example":"CORPORATE"},"rtEnabled":{"type":"boolean","description":"Whether real-time voice conversations are enabled","example":false},"rtAdminOnly":{"type":"boolean","description":"Whether real-time conversations are restricted to admins","example":true},"created":{"type":"string","format":"date-time","example":"2025-07-17T18:20:34.330Z"},"id":{"type":"string","example":"68793ef2a8a4a5eaff36e7ca"}}},"GeneratePromptPreviewRequest":{"type":"object","required":["questions"],"properties":{"ainame":{"type":"string","description":"Name of the Digital Twin being created","example":"Dr. Smith"},"questions":{"type":"array","description":"Interview Q&A pairs","items":{"type":"object","required":["question","answer"],"properties":{"code":{"type":"string","description":"Question bank code","example":"EXPERTISE"},"question":{"type":"string","description":"The interview question","example":"What is your area of expertise?"},"objectif":{"type":"string","description":"Purpose of the question"},"answer":{"type":"string","description":"User's answer","example":"Machine learning and neural networks"}}}}}},"GeneratePromptPreviewResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"data":{"type":"string","description":"Generated persona instructions text"}}}}},"SdkSignRequest":{"type":"object","required":["params","institutionId"],"properties":{"params":{"type":"object","description":"Launch parameters to be HMAC-signed. All values are canonicalized (converted to strings\nand `launch_*` keys are stripped) before signing to ensure consistency with the verify side,\nsince URL query strings coerce all values to strings.\n","properties":{"email":{"type":"string","format":"email","example":"john.doe@domain.edu"},"profilename":{"type":"string","example":"John Doe"},"usertype":{"type":"integer","description":"LMS user type (1=student, 2+=instructor/admin)","example":4},"userid":{"type":"integer","example":110},"roleid":{"type":"integer","example":123},"rolename":{"type":"string","example":"Course ABC"},"partnerid":{"type":"integer","example":1},"partnername":{"type":"string","example":"ABC Global Inc."},"lticontextid":{"type":"string","example":"https://domain.edu/course/123"},"digitaltwin":{"type":"boolean","description":"When true with empty institutionId, enters digital twin selector mode","example":true},"task":{"type":"string","example":"do"},"institutionid":{"type":"string","format":"uuid","description":"Institution public ID (may be empty for digital twin mode)","example":"f831501f-b645-481a-9cbb-331509aaf8c1"},"institutionurl":{"type":"string","example":"https://domain.edu"},"clientip":{"type":"string","example":"134.123.234.234"}}},"institutionId":{"type":"string","description":"Institution public UUID. Required for institution-specific launches.\nMay be an empty string `\"\"` for digital twin selector mode (when `params.digitaltwin` is true),\nwhich skips institution lookup and origin validation.\n","example":"f831501f-b645-481a-9cbb-331509aaf8c1"}}},"SdkSignResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"launch_token":{"type":"string","description":"HMAC-SHA256 signature of the canonicalized launch parameters","example":"a1b2c3d4e5f6..."},"nonce":{"type":"string","description":"Cryptographic nonce (32 hex chars) to prevent replay attacks","example":"f47ac10b58cc4372a5670e02b2c3d479"},"timestamp":{"type":"integer","description":"Unix timestamp (seconds) when the token was issued","example":1740500000}}},"SdkVerifyRequest":{"type":"object","required":["params","launch_token","nonce","timestamp"],"properties":{"params":{"type":"object","description":"The launch parameters to verify. May include `launch_token`, `launch_nonce`, and\n`launch_timestamp` keys (these are stripped during canonicalization before HMAC comparison).\nAll values are stringified to match the sign-side canonicalization.\n"},"launch_token":{"type":"string","description":"The HMAC-SHA256 token returned from sdk-sign","example":"a1b2c3d4e5f6..."},"nonce":{"type":"string","description":"The nonce returned from sdk-sign","example":"f47ac10b58cc4372a5670e02b2c3d479"},"timestamp":{"type":"integer","description":"The timestamp returned from sdk-sign (must be within 10-minute window)","example":1740500000}}},"SdkVerifyResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true}}},"ApiKeySigninResponse":{"type":"object","properties":{"token":{"type":"string","description":"JWT signed for the API-key-bound user. Use it in subsequent calls via\n`Authorization: Bearer <token>` or the `x-access-token` header. Token TTL\nmatches the normal signin flow (6 hours by default, configurable via\n`JWT_VALIDITY_SEC`).\n","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"profile":{"type":"object","description":"Minimal profile envelope tailored for SDK / integration consumers.\nSmaller than the regular signin profile — see properties below.\n","properties":{"_id":{"type":"string","example":"6430736fd62d650040420674"},"email":{"type":"string","format":"email","example":"integration-bot@praxis-ai.com"},"fname":{"type":"string","example":"Integration"},"lname":{"type":"string","example":"Bot"},"accountType":{"type":"string","enum":["admin","super"],"description":"API-key signin is gated to admin/super accounts only.","example":"admin"},"plan":{"type":"string","example":"pro"},"status":{"type":"string","example":"active"},"credits":{"type":"integer","example":1000},"creditsUsed":{"type":"integer","example":12},"institution":{"type":"object","nullable":true,"description":"Trimmed institution summary (only set when the user belongs to one).","properties":{"_id":{"type":"string","example":"68793ef2a8a4a5eaff36e7ca"},"name":{"type":"string","example":"domain.edu"},"status":{"type":"string","example":"active"},"credits":{"type":"integer","example":500},"ainame":{"type":"string","example":"Hugo"}}}}}}},"MfaVerifyRequest":{"type":"object","required":["challengeId","code"],"properties":{"challengeId":{"type":"string","description":"Opaque server-issued challenge identifier returned by POST /api/auth/signin (or /autosignup) when the response carried mfaRequired=true.","example":"65f2c9a1d8e4b3c5a1234567"},"code":{"type":"string","pattern":"^[0-9]{6}$","description":"The 6-digit numeric code the user received by email.","example":"472918"}}},"MfaChallengeIdRequest":{"type":"object","required":["challengeId"],"properties":{"challengeId":{"type":"string","description":"Opaque server-issued challenge identifier.","example":"65f2c9a1d8e4b3c5a1234567"}}},"MfaResendResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"challengeId":{"type":"string","description":"Identifier of the freshly-issued challenge. The prior challenge is invalidated; future verify / resend / cancel calls must use this new id.","example":"65f2cabe1234e5f6a8765432"},"maskedEmail":{"type":"string","example":"hu***@praxis-ai.com"}}},"MfaVerifyError":{"type":"object","properties":{"success":{"type":"boolean","example":false},"code":{"type":"string","enum":["BAD_REQUEST","WRONG_CODE","CHALLENGE_EXPIRED","TOO_MANY_ATTEMPTS","USER_MISSING","RESEND_COOLDOWN","MFA_NOT_CONFIGURED","MFA_EMAIL_FAILED"]},"message":{"type":"string"},"attemptsRemaining":{"type":"integer","description":"Only present on WRONG_CODE / TOO_MANY_ATTEMPTS responses.","example":4},"retryAfter":{"type":"integer","description":"Seconds the client should wait before retrying. Present on RESEND_COOLDOWN.","example":24}}},"TrustedDevice":{"type":"object","properties":{"id":{"type":"string","description":"Mongo ObjectId of the trusted_device row"},"deviceId":{"type":"string","description":"Opaque 32-byte hex handle carried in the pria_mfa_trust cookie"},"sourceIp":{"type":"string","example":"10.0.0.5"},"ua":{"type":"string","example":"Mozilla/5.0 ..."},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"}}},"ApiKeyUsageDevice":{"type":"object","description":"One per-device aggregate row for the caller's personal API key. Bumped on every successful /api/auth/api-key-signin from the same (ip /24, UA family, UA OS) fingerprint.","properties":{"id":{"type":"string","description":"Mongo ObjectId of the api_key_usage row (used to DELETE)."},"sourceIp":{"type":"string","example":"203.0.113.42","description":"First observed exact IP (display only)."},"ipSubnet24":{"type":"string","example":"203.0.113.0/24","description":"Network subnet used in the device fingerprint."},"ua":{"type":"string","example":"Mozilla/5.0 ...","description":"First observed User-Agent string (capped at 512 chars)."},"uaFamily":{"type":"string","example":"Chrome"},"uaOs":{"type":"string","example":"macOS"},"uaVersion":{"type":"string","example":"120.0.0.0"},"clientName":{"type":"string","description":"Optional x-pria-client-name header (control-stripped, ≤32 chars)."},"clientHostname":{"type":"string","description":"Optional x-pria-client-hostname header (control-stripped, ≤64 chars)."},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"usageCount":{"type":"integer","example":42}}},"AssistantUserData":{"type":"object","description":"Denormalized user data populated from the user reference","properties":{"email":{"type":"string"},"fname":{"type":"string"},"lname":{"type":"string"},"institution":{"type":"string","description":"Institution ObjectId from the user record"},"accountType":{"type":"string"}}},"AssistantInstitutionData":{"type":"object","description":"Denormalized institution data populated from the institution reference","properties":{"name":{"type":"string"},"ainame":{"type":"string"},"status":{"type":"string"},"picture":{"type":"string"}}},"AssistantAccountData":{"type":"object","description":"Denormalized account data resolved from the institution's account reference","properties":{"_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}}},"Assistant":{"type":"object","properties":{"_id":{"type":"string","description":"Unique identifier for the assistant"},"name":{"type":"string","description":"Name of the assistant"},"description":{"type":"string","description":"Description of the assistant's functionality"},"instructions":{"type":"string","description":"Detailed instructions for the assistant. Omitted for non-super users viewing system assistants (user=null), and omitted when lean=true."},"picture_url":{"type":"string","description":"URL to the assistant's picture"},"status":{"type":"string","enum":["active","unpublished","inactive","deleted"],"description":"Current status of the assistant"},"liked_count":{"type":"integer","description":"Number of likes for this assistant"},"admin_only":{"type":"boolean","description":"Whether the assistant is restricted to admin users only"},"institution_shared":{"type":"boolean","description":"Whether the assistant is shared with the institution"},"account_shared":{"type":"boolean","description":"Whether the assistant is shared across all institutions in the same account. Requires institution_shared to be true."},"editable_others":{"type":"boolean","description":"Whether other users can edit this assistant"},"remember_history":{"type":"integer","description":"Number of conversation history items to remember"},"ragEnabled":{"type":"boolean","description":"Whether RAG (retrieval-augmented generation) is enabled"},"bypassSystemContext":{"type":"boolean","description":"Whether to bypass the system context prompt"},"conversationModel":{"type":"string","description":"Optional model override for this assistant (max 200 chars)"},"stsConversationModel":{"type":"string","description":"Optional STS/voice conversation model; used only in ElevenLabs/Anam/LemonSlice voice mode (max 200 chars)"},"argument_5":{"type":"string","nullable":true,"description":"Optional argument field. Note: argument_1 through argument_4 are stripped from list responses."},"user":{"type":"string","description":"User ObjectId who created the assistant. Null for system assistants.","nullable":true},"institution":{"type":"string","description":"Institution ObjectId this assistant belongs to","nullable":true},"user_data":{"$ref":"#/components/schemas/AssistantUserData"},"institution_data":{"$ref":"#/components/schemas/AssistantInstitutionData"},"account_data":{"$ref":"#/components/schemas/AssistantAccountData"},"created":{"type":"string","format":"date-time","description":"Creation timestamp"}},"nullable":true,"description":"Most recently used assistant in this course. Mobile/web use this to restore conversation context on the next turn. Null if the assistant was deleted or not found."},"Collection":{"type":"object","properties":{"_id":{"type":"string","description":"Unique identifier"},"name":{"type":"string","description":"Collection display name"},"user":{"type":"string","description":"Owner user ID"},"institution":{"type":"string","description":"Institution ID for vault scoping"},"account_shared":{"type":"boolean","description":"Whether the collection is account-shared"},"parent":{"type":"string","nullable":true,"description":"Parent collection ID for sub-collections (null for root)"},"color":{"type":"string","nullable":true,"pattern":"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$","description":"Optional accent color as `#RGB` or `#RRGGBB` hex. When set, the UI renders the folder icon as solid tinted with this value; otherwise the default outline icon is used.","example":"#3b82f6"},"created":{"type":"string","format":"date-time"},"_type":{"type":"string","enum":["collection"]},"fileCount":{"type":"integer","description":"Total non-deleted files"},"totalSize":{"type":"integer","description":"Sum of file sizes in bytes"},"includedCount":{"type":"integer","description":"Files with status selected"},"excludedCount":{"type":"integer","description":"Files with status active"},"subCollectionCount":{"type":"integer","description":"Number of direct child sub-collections"},"previewFiles":{"type":"array","items":{"type":"string"},"description":"First 6 filenames for preview"}}},"EmbeddingChunk":{"type":"object","properties":{"_id":{"type":"string","description":"Unique identifier for the embedding chunk","example":"665a1b2c3d4e5f6789012345"},"chunkText":{"type":"string","description":"The text content of this chunk (max ~32K characters)","example":"This is a paragraph from the uploaded document..."},"chunkLen":{"type":"integer","description":"Character length of the chunk text","example":512},"chunkIndex":{"type":"integer","description":"Zero-based position of this chunk within the parent upload","example":0},"upload":{"type":"string","description":"ID of the parent upload this chunk belongs to","example":"665a1b2c3d4e5f6789012300"},"chunkUrl":{"type":"string","description":"Location reference within the source document (e.g., page number for PDFs, element ID for HTML)","example":"#page=2"},"created":{"type":"string","format":"date-time","description":"Timestamp when this chunk was created"},"usage":{"type":"integer","description":"Token count consumed when generating the vector embedding","example":128}}},"GetEmbeddingsRequest":{"type":"object","required":["upload"],"properties":{"upload":{"type":"string","description":"The ID of the upload whose embedding chunks to retrieve","example":"665a1b2c3d4e5f6789012300"}}},"GetEmbeddingsResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","description":"Embedding chunks sorted by chunkIndex (max 1000)","items":{"$ref":"#/components/schemas/EmbeddingChunk"}},"embeddingModel":{"type":"object","nullable":true,"description":"Model actually used to generate these chunks' vectors. Null when the upload has no recorded embedding model.","properties":{"name":{"type":"string","example":"text-embedding-3-small"},"provider":{"type":"string","example":"openai_cli"},"maxInputTokens":{"type":"integer","example":8191}}},"currentEmbeddingModel":{"type":"object","nullable":true,"description":"The institution's currently-configured embedding model (what a re-ingestion would use). Compare against `embeddingModel` to detect drift.","properties":{"name":{"type":"string","example":"text-embedding-3-small"},"provider":{"type":"string","example":"openai_cli"}}},"chunkMaxChars":{"type":"integer","description":"Soft cap (characters) for segment edit/create UI. Matches the server-side RAG chunk-size constant so admin edits stay in lockstep with new ingestion output.","example":8000}}},"UpdateEmbeddingRequest":{"type":"object","properties":{"chunkText":{"type":"string","description":"Updated chunk text. When modified, the vector embedding is automatically regenerated.","example":"Updated paragraph text from the document..."}}},"UpdateEmbeddingResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Embedding updated!"}}},"DeleteEmbeddingResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Embedding deleted!"}}},"CreateEmbeddingRequest":{"type":"object","required":["upload","chunkText"],"properties":{"upload":{"type":"string","description":"The ID of the upload to add the embedding chunk to","example":"665a1b2c3d4e5f6789012300"},"chunkText":{"type":"string","description":"The text content for the new chunk (max ~32K characters). A vector embedding is generated automatically.","example":"This is a new manually-added paragraph for RAG search..."}}},"CreateEmbeddingResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Embedding created!"},"data":{"$ref":"#/components/schemas/EmbeddingChunk"}}},"SanitizeEmbeddingResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"sanitizedText":{"type":"string","description":"The AI-cleaned segment text. Not persisted — use PUT /embedding/{id} to save.","example":"This is the cleaned paragraph with noise removed and formatting normalized..."},"tokensUsed":{"type":"integer","description":"Token count consumed by the sanitization LLM call (tallied to parent Upload)","example":142},"model":{"type":"string","description":"The LLM model used for sanitization","example":"anthropic.claude-3-haiku-20250514-v1:0"}}},"FeedbackRequest":{"type":"object","required":["feedback"],"properties":{"feedback":{"type":"string","maxLength":4096,"description":"User feedback text","example":"Great feature! Very helpful."}}},"FeedbackCreateSuccess":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Feedback created!"}}},"Feedback":{"type":"object","properties":{"_id":{"type":"string","description":"Feedback document ID"},"feedback":{"type":"string","description":"Feedback text"},"response":{"type":"string","description":"Admin response to the feedback (default: empty string)"},"created":{"type":"string","format":"date-time","description":"When the feedback was created"},"email":{"type":"string","description":"Email associated with the feedback"},"user":{"type":"string","description":"ObjectId of the user who submitted the feedback"},"institution":{"type":"string","description":"ObjectId of the user's institution"},"status":{"type":"string","description":"Feedback status (default: active)","example":"active"},"__v":{"type":"integer","description":"Mongoose version key"}}},"UploadUrlRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"URL to download and ingest content from","example":"https://example.com/document.pdf"},"confidential":{"type":"boolean","description":"Whether the content is confidential. Only applied when institution is provided; ignored for personal uploads.","default":false},"skipIndexing":{"type":"boolean","description":"When true, the file is stored in the vault but RAG embedding generation is skipped. The file is set to status 'active' (visible in vault) with no embeddings. Default: false.","default":false},"scrape":{"type":"boolean","description":"When true, scrapes webpage content. When false, downloads the file directly.","default":false},"institution":{"type":"string","description":"Institution ID to associate the upload with. Must match the user's institution for regular users.","example":"6631915765bb0a94cfd6ca99"},"selectedCourse":{"type":"object","description":"Course context for the upload history record","properties":{"course_id":{"type":"number","description":"Unique identifier for the course"},"course_name":{"type":"string","description":"Name of the course"},"assistant":{"type":"object","properties":{"_id":{"type":"string","description":"Assistant unique identifier"}}}}},"selectedAssistant":{"type":"string","description":"Assistant ID for the upload history record (used as fallback if selectedCourse.assistant._id is not set)","example":"6856fa89cbafcff8d98680f5"},"collection":{"type":"string","description":"Collection ID to associate the uploaded file with. When provided, the Upload record is linked to this collection.","example":"6631915765bb0a94cfd6ca99"},"account_shared":{"type":"boolean","description":"When true and institution is set, marks the file as shared across the institution's account","default":false}}},"UploadUrlResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"URL ingested successfully"},"code":{"type":"integer","description":"Status code (1 indicates success)","example":1}}},"FileConfidentialRequest":{"type":"object","required":["confidential"],"properties":{"confidential":{"type":"boolean","description":"Whether the file should be marked as confidential (is_private)","example":true}}},"FileConfidentialResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"File marked as confidential"},"is_private":{"type":"boolean","description":"The updated confidential status of the file","example":true}}},"ProcessedFileResult":{"type":"object","description":"Result for each processed file in the upload batch","properties":{"fieldname":{"type":"string","example":"files"},"originalname":{"type":"string","description":"Original filename as uploaded"},"filename":{"type":"string","description":"System-generated filename on disk"},"mimetype":{"type":"string","description":"MIME type of the file"},"size":{"type":"integer","description":"File size in bytes"},"success":{"type":"boolean","description":"Whether this individual file was processed successfully"},"message":{"type":"string","description":"Status message for this file"},"usage":{"type":"integer","description":"Tokens used for RAG processing of this file"},"uploadId":{"type":"string","description":"MongoDB ObjectId of the created Upload record"},"file_url":{"type":"string","format":"uri","description":"URL to access the file"},"file_title":{"type":"string","description":"AI-generated title for the file"},"file_summary":{"type":"string","description":"AI-generated summary of file content"},"summary_model":{"type":"string","description":"AI model used for summarization"},"image_analysis_model":{"type":"string","description":"AI model used for image analysis (if applicable)"},"embeddings_model":{"type":"string","description":"AI model used for embeddings generation"},"is_public":{"type":"boolean","description":"Whether the file is publicly accessible"},"is_private":{"type":"boolean","description":"Whether the file is marked as confidential"},"status":{"type":"string","description":"Current status of the upload record"},"thumbnail":{"type":"string","description":"Base64-encoded thumbnail image"},"queued":{"type":"boolean","description":"Present and true when the file was handed off to the ingestion queue. Processing continues asynchronously — poll the Upload record to watch ingestion.phase transition to 'done' or 'error'."},"ingestion":{"type":"object","description":"Ingestion job state. Only present when the file has been queued or has a terminal outcome.","properties":{"phase":{"type":"string","enum":["queued","extract","chunk","sanitize","embed","finalize","done","error"]},"progress":{"type":"integer","description":"Percent complete within the current phase (0-100)"},"attempts":{"type":"integer"},"enqueuedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"lastError":{"type":"string"},"errorClass":{"type":"string","enum":["permanent","transient","unknown"]}}}}},"ReprocessFileResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"queued":{"type":"boolean","example":true},"mode":{"type":"string","enum":["full","embed"],"description":"The reprocess mode that was applied","example":"full"},"message":{"type":"string","example":"File re-queued for processing"},"uploadId":{"type":"string","description":"MongoDB ObjectId of the re-queued Upload record"},"ingestion":{"type":"object","properties":{"phase":{"type":"string","example":"queued"},"attempts":{"type":"integer","example":0},"enqueuedAt":{"type":"string","format":"date-time"}}}}},"UploadFilesResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Upload completed"},"files":{"type":"array","items":{"$ref":"#/components/schemas/ProcessedFileResult"},"description":"Array of per-file processing results"}}},"DeleteFileResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"File(s) deleted successfully"},"code":{"type":"integer","description":"Status code (1 indicates success)","example":1}}},"ReloadFileResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"File reloaded successfully!"},"data":{"$ref":"#/components/schemas/UploadFile"}}},"MulterErrorResponse":{"type":"object","description":"Error returned by the multer upload middleware before reaching the handler","properties":{"error":{"type":"string","description":"Error category","example":"File too large"},"message":{"type":"string","description":"Human-readable error message","example":"File size exceeds 500MB limit"}}},"HistoryQuery":{"type":"object","properties":{"limit":{"type":"integer","description":"Maximum number of records to return (default 100)","example":5},"institution":{"type":"string","description":"Institution ObjectId. If omitted, uses the current user's institution."},"course_id":{"type":"number","description":"Course identifier to filter by. When 0, matches records with course_id 0 or null.","example":1750532703472},"before":{"type":"number","description":"Epoch timestamp in ms. Returns records created before this date.","example":1723019070274},"after":{"type":"number","description":"Epoch timestamp in ms. Returns records created after this date.","example":1723019070274},"historyId":{"type":"string","description":"The ObjectId of a specific history record to retrieve","example":"688b024f7db6fe6e921399e3"},"search":{"type":"string","description":"Free-text search string. Matches against input, inputs, output, and outputs fields (case-insensitive regex).","example":"how to deploy"},"tools":{"type":"boolean","description":"When true, returns full tool response data. When false or omitted, tool responses longer than 80 chars are truncated.","example":false},"allInstitutions":{"type":"boolean","description":"When true, search across all institutions the user is enrolled in PLUS the user's personal account (histories with no institution). Overrides the `institution` field if both are set, and causes `course_id` to be ignored (cross-twin scope always crosses courses). Populates `institution` as an object ({_id, name, ainame, picture}) for institutional records, or as `{personal: true}` for personal-account records.","default":false}}},"HistoryRecord":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the history record"},"created":{"type":"string","format":"date-time","description":"Creation timestamp"},"credits":{"type":"integer","description":"Credits consumed for this interaction"},"usage":{"type":"integer","description":"Token usage count"},"cached":{"type":"integer","description":"Cached tokens used"},"completion":{"type":"integer","description":"Completion tokens generated"},"discount":{"type":"number","description":"Credit discount applied to this interaction"},"latencyMs":{"type":"integer","description":"Response latency in milliseconds"},"ragDurationMs":{"type":"integer","description":"RAG vector search duration in milliseconds"},"hasRagSearch":{"type":"boolean","description":"True when this turn ran retrieval and produced at least one segment. The full structured array is NOT included in list responses — it can be multi-KB per row (N chunks × multi-KB chunkText) and is fetched lazily via `GET /api/user/history/{id}/ragSearch` on `<details>` expand.\n"},"ragSearchCount":{"type":"integer","description":"Number of retrieved segments for this turn (used by the UI summary line)."},"ragSearchMode":{"type":"string","enum":["RAG","KAG"],"description":"Retrieval mode this turn was captured under (KAG when fusion is enabled, otherwise RAG)."},"institution":{"description":"Institution this record belongs to. Normally an ObjectId string reference. When the request sets `allInstitutions: true` (cross-twin search), this is populated as an object with _id, name, ainame, and picture — or as `{personal: true}` for personal-account records.","oneOf":[{"type":"string","description":"ObjectId of the institution this record belongs to"},{"type":"object","description":"Populated institution details (returned when `allInstitutions: true`)","properties":{"_id":{"type":"string","description":"Institution unique identifier"},"name":{"type":"string","description":"Institution name"},"ainame":{"type":"string","description":"Institution AI display name"},"picture":{"type":"string","description":"Institution picture URL"},"personal":{"type":"boolean","description":"True when the record belongs to the user's personal account (no institution)."}}}]},"user":{"type":"string","description":"ObjectId of the user who owns this record"},"favorite":{"type":"boolean","description":"Whether the record is marked as favorite"},"forgotten":{"type":"boolean","description":"Whether the record is soft-deleted"},"course_id":{"type":"number","description":"Associated course identifier"},"course_name":{"type":"string","description":"Associated course name"},"role_id":{"type":"string","description":"Role identifier used during this interaction"},"role_name":{"type":"string","description":"Role name used during this interaction"},"query_duration_ms":{"type":"integer","description":"Query processing duration in milliseconds"},"conversation_model":{"type":"string","description":"AI model used for the conversation"},"success":{"type":"boolean","description":"Whether the dialogue completed successfully"},"message":{"type":"string","description":"Status or error message from the dialogue"},"thumbUpDown":{"type":"string","description":"User feedback on the response (thumb up or down)"},"favorite_name":{"type":"string","description":"Custom name assigned when the record is favorited"},"in":{"type":"object","description":"User input data","properties":{"input":{"type":"string","description":"Primary user input text. Trimmed to 200 chars in list views."},"inputs":{"type":"array","items":{"type":"string"},"description":"Alternate plural input array. Trimmed to 200 chars total in list views."}}},"out":{"type":"object","description":"AI response data","properties":{"output":{"type":"string","description":"Primary AI response text (singular). Trimmed to 200 chars in list views."},"outputs":{"type":"array","items":{"type":"string"},"description":"AI response outputs array. Trimmed to 200 chars total in list views."},"code":{"type":"string","description":"Code block content from the AI response"},"code_language":{"type":"string","description":"Programming language of the code block"}}},"error":{"type":"string","description":"Error message if the dialogue failed. Only present when an error occurred."},"assistant":{"type":"object","nullable":true,"description":"Assistant associated with this record. Null if assistant was deleted.","properties":{"_id":{"type":"string","description":"Assistant unique identifier"},"name":{"type":"string","description":"Assistant name"},"liked_count":{"type":"integer","description":"Number of likes for this assistant"},"picture_url":{"type":"string","description":"Assistant avatar image URL. Omitted when assistant name contains 'New Conversation'."}}},"tools":{"type":"array","description":"Tools used during the interaction. Tool responses are truncated to 80 chars unless tools=true in the request.","items":{"type":"object","properties":{"id":{"type":"string","description":"Tool execution identifier"},"name":{"type":"string","description":"Tool name used"},"arguments":{"type":"object","description":"Arguments passed to the tool"},"response":{"type":"string","description":"Tool response text. Truncated to 80 chars with '...' suffix when partialResponse is true."},"responseLength":{"type":"integer","description":"Full length of the original tool response"},"partialResponse":{"type":"boolean","description":"True when response was truncated (responseLength > 80 and tools param not set)"},"success":{"type":"boolean","description":"Whether tool execution was successful"},"_id":{"type":"string","description":"Tool execution record identifier"}}}}}},"HistoriesResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"type":"array","items":{"$ref":"#/components/schemas/HistoryRecord"},"description":"Array of history records sorted oldest-first (dialogs are reversed before returning)"},"matched":{"type":"integer","description":"Only returned when `allInstitutions: true`. Total number of records matching the search (not capped by `limit`). Equals `total` when no `search` term is provided."},"total":{"type":"integer","description":"Only returned when `allInstitutions: true`. Total number of records in the searchable scope (ignores the `search` term; still respects `allInstitutions`, `course_id`, `before`, `after`, and `status/forgotten` filters)."},"message":{"type":"string","description":"Error message when success is false"},"error":{"type":"object","description":"Error object when success is false"}}},"FavoritesQuery":{"type":"object","properties":{"institution":{"type":"string","description":"Institution ObjectId. If omitted, uses the current user's institution."}}},"FavoritesResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"type":"array","items":{"$ref":"#/components/schemas/HistoryRecord"},"description":"Array of favorited history records (max 1000). Inputs/outputs trimmed to 200 chars. Tool responses truncated to 80 chars."},"message":{"type":"string","description":"Error message when success is false"},"error":{"type":"object","description":"Error object when success is false"}}},"CoursesQuery":{"type":"object","properties":{"institution":{"type":"string","nullable":true,"description":"Institution ObjectId. Three-state semantics: (1) valid ObjectId scopes to that twin, (2) explicit null/empty scopes to personal/null history, (3) field omitted falls back to the user's current institution (or personal if none)."}}},"Course":{"type":"object","properties":{"course_id":{"type":"number","description":"Unique identifier for the course. Courses with course_id=0 are filtered out."},"course_name":{"type":"string","description":"Name of the course"},"assistant":{"$ref":"#/components/schemas/Assistant"},"history_count":{"type":"integer","description":"Number of dialogue entries in the course history"},"last_dialogue_date":{"type":"string","format":"date-time","description":"ISO timestamp of the most recent dialogue in the course"}}},"CourseListResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Course"},"description":"Array of course objects sorted by last_dialogue_date descending. Courses with course_id=0 are excluded."},"message":{"type":"string","description":"Error message when success is false"},"error":{"type":"object","description":"Error object when success is false"}}},"UpdateHistoryRequest":{"type":"object","description":"Accepts any properties to update on the history document. Common fields include favorite and forgotten, but any valid history field is accepted.","properties":{"favorite":{"type":"boolean","description":"Whether to mark the history item as favorite","example":true},"forgotten":{"type":"boolean","description":"Whether to soft-delete the dialogue from history","example":false},"favorite_name":{"type":"string","description":"Custom name for a favorited record","example":"Deployment Guide"},"course_name":{"type":"string","description":"Update the course name for this record"},"thumbUpDown":{"type":"string","description":"User feedback on the response","example":"up"}}},"UpdateHistoryResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Whether the operation was successful","example":true},"data":{"type":"object","description":"The full updated history document"},"message":{"type":"string","description":"Status message","example":"History updated!"}}},"ClearHistoryRequest":{"type":"object","description":"Two mutually exclusive modes. Provide exactly one of: course_id, or id. The course_id mode also accepts an optional institution scope.","properties":{"course_id":{"type":"number","description":"Course identifier. Detaches favorites from the course (unsets course_id/course_name) then soft-deletes remaining course records within the resolved institution scope. course_id=0 sweeps the unassigned bucket (records with course_id 0, null, or missing).","example":1750532703472},"institution":{"type":"string","nullable":true,"description":"Institution scope (course_id mode only). Three-state: (1) valid ObjectId scopes to that twin (membership-checked — 403 if the user is not an active member), (2) explicit null/empty scopes to personal/null history, (3) field omitted falls back to the user's current institution. Malformed ObjectId returns 400."},"id":{"type":"string","description":"ObjectId of a single history record to soft-delete. Single-record operations are not institution-scoped (the _id is globally unique).","example":"688b024f7db6fe6e921399e3"}}},"ClearHistoryResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"History updated!"}}},"RenameHistoryRequest":{"type":"object","description":"Two mutually exclusive modes. Either rename a conversation (course_id + course_name) or rename a favorite (id + favorite_name). Conversation renames support institution scoping.","properties":{"course_id":{"type":"number","description":"Course identifier for renaming a conversation. Must be paired with course_name. course_id=0 sweeps the unassigned bucket.","example":1750532703472},"course_name":{"type":"string","description":"New name for the conversation. Used with course_id.","example":"My Renamed Conversation"},"institution":{"type":"string","nullable":true,"description":"Institution scope (course_id mode only). Three-state: (1) valid ObjectId scopes to that twin (membership-checked — 403 if not active), (2) explicit null/empty scopes to personal/null history, (3) field omitted falls back to user.institution. Malformed ObjectId returns 400."},"id":{"type":"string","description":"ObjectId of the favorite record to rename. Must be paired with favorite_name. Single-record operations are not institution-scoped (_id is globally unique).","example":"688b024f7db6fe6e921399e3"},"favorite_name":{"type":"string","description":"New name for the favorite. If falsy (empty string or null), the favorite_name field is removed via $unset.","example":"Deployment Guide"}}},"RenameHistoryResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","description":"Either 'Conversation renamed!' or 'Favorite renamed!' depending on the mode used","example":"Conversation renamed!"}}},"SummarizeHistoryRequest":{"type":"object","description":"Supply either 'id' to summarize a single history record (favorite), or 'course_id' to summarize a whole conversation. course_id mode supports institution scoping.","properties":{"id":{"type":"string","description":"ObjectId of the history record to summarize. Single-record operations are not institution-scoped (_id is globally unique).","example":"688b024f7db6fe6e921399e3"},"course_id":{"type":"integer","description":"Conversation course_id to summarize (first 2 + last 3 dialogues; all if ≤5 total). course_id=0 covers the unassigned bucket (course_id 0/null/missing).","example":1712345678901},"institution":{"type":"string","nullable":true,"description":"Institution scope (course_id mode only). Three-state: (1) valid ObjectId scopes to that twin (membership-checked — 403 if not active), (2) explicit null/empty scopes to personal/null history, (3) field omitted falls back to user.institution. Malformed ObjectId returns 400."}}},"SummarizeHistoryResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Summary generated"},"data":{"type":"string","description":"AI-generated title. Favorites (id mode): ≤200 chars. Conversations (course_id mode): ≤50 chars.","example":"How to deploy a Node.js application to AWS with Docker"}}},"MembershipErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"},"error":{"type":"object","description":"Error details (included on catch blocks)"}}},"UserMemoryRow":{"type":"object","properties":{"_id":{"type":"string"},"key_name":{"type":"string"},"key_value":{"type":"string"},"key_description":{"type":"string"},"key_namespace":{"type":"string"},"shared":{"type":"boolean"},"created":{"type":"string","format":"date-time"},"institution":{"nullable":true,"type":"object","description":"Institution metadata for shared rows. Null on personal entries.\nFields are projected from the Institution model and reflect that\ninstitution's branding (its picture and AI display name).\n","properties":{"_id":{"type":"string"},"name":{"type":"string"},"ainame":{"type":"string","description":"AI display name for the institution (e.g. \"Pria\"). Falls back to \"\" when unset."},"picture":{"type":"string","description":"Branded avatar URL for the institution. Empty string when no picture is configured."}}},"editable":{"type":"boolean","description":"True if the requesting user can edit/delete this row. Predicate:\nuser authored it, OR (shared row in their institution AND user\nis super OR holds institutions.edit on that institution).\n"},"isAuthor":{"type":"boolean"}}},"MyDataStats":{"type":"object","properties":{"success":{"type":"boolean","example":true},"dialogues":{"type":"object","properties":{"count":{"type":"integer","description":"Number of personal-scope, non-forgotten history rows"},"earliest":{"type":"string","format":"date-time","nullable":true,"description":"Earliest created timestamp across counted rows; null on empty"},"latest":{"type":"string","format":"date-time","nullable":true,"description":"Latest created timestamp; null on empty"}}},"uploads":{"type":"object","properties":{"count":{"type":"integer","description":"Personal uploads with status != 'deleted'"},"totalBytes":{"type":"integer","description":"Sum of filesize across the counted uploads"}}},"memory":{"type":"object","properties":{"entries":{"type":"integer","description":"Personal memory rows with status == 'active'"}}},"assistants":{"type":"object","properties":{"count":{"type":"integer","description":"Personal Assistant rows with status != 'deleted'"}}},"feedback":{"type":"object","properties":{"count":{"type":"integer","description":"Feedback rows authored by the user with status != 'deleted' (scope is `user` only — feedback is the user's speech to Pria regardless of institutional context)"}}},"sessions":{"type":"object","properties":{"count":{"type":"integer","description":"Login session records (device / IP / browser fingerprint). Scope is `user` only — sessions naturally span institutions."}}}}},"MyDataExportResponse":{"type":"object","properties":{"status":{"type":"string","enum":["queued"]},"estimatedSeconds":{"type":"integer","description":"Rough wait before the email arrives; not a hard SLA"}}},"MyDataDeleteRequest":{"type":"object","description":"At least one of `dialogues`, `vault`, `memory`, `assistants`, `feedback`, or `sessions` must be true.","properties":{"dialogues":{"type":"boolean","description":"Flip personal History.forgotten to true (records survive for billing aggregates). Cascades: historyCompactionCache rows derived from these dialogues are hard-deleted."},"vault":{"type":"boolean","description":"Soft-delete personal uploads (status='deleted'), remove embeddings + physical file"},"memory":{"type":"boolean","description":"Hard-delete the user's personal Memory rows (institution:null). Mirrors DELETE /api/user/memory/personal scope semantics."},"assistants":{"type":"boolean","description":"Soft-delete the user's personal Assistant rows (status -> 'deleted'). Rows survive for historical references (audit, conversation backfill) but are hidden from the chooser."},"feedback":{"type":"boolean","description":"Soft-delete Feedback rows authored by the user (status -> 'deleted'). Scope is `user` only — institutional context at submission time is ignored."},"sessions":{"type":"boolean","description":"Hard-delete login session records (IP / browser / device fingerprints). Does NOT log the user out of other tabs — the JWT cookie is independent of these audit rows."}}},"MyDataDeleteResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"dialoguesForgotten":{"type":"integer"},"uploadsDeleted":{"type":"integer"},"bytesFreed":{"type":"integer"},"memoryDeleted":{"type":"integer","description":"Personal Memory rows hard-deleted by this request"},"assistantsDeleted":{"type":"integer","description":"Personal Assistant rows soft-deleted by this request"},"feedbackDeleted":{"type":"integer","description":"Feedback rows soft-deleted by this request"},"sessionsDeleted":{"type":"integer","description":"Session records hard-deleted by this request"},"compactionCachePurged":{"type":"integer","description":"HistoryCompactionCache rows hard-deleted as a cascade from dialogues delete"},"uploadsFailed":{"type":"array","items":{"type":"object","properties":{"uploadId":{"type":"string"},"error":{"type":"string"}}},"description":"Per-upload failures from the rag.fileDelete call (status flip is skipped on these so a retry can re-attempt)"}}},"InstitutionCreditCaps":{"type":"object","description":"Per-institution user-quota configuration. Admins with the institutions.edit entitlement may set these when the account has not enabled caps; when the account owns caps the institution inherits them read-only.","properties":{"enabled":{"type":"boolean","default":false,"description":"When true and the account has not enabled caps, per-user quotas are enforced for this institution's members."},"perUserInstitution":{"type":"number","nullable":true,"description":"Maximum lifetime credits a user may consume in this institution. Null or 0 = no cap."},"perUser24h":{"type":"number","nullable":true,"description":"Maximum credits a user may consume in any rolling 24-hour window. Null or 0 = no cap. Auto-recovers."}}},"AIModel":{"type":"object","properties":{"_id":{"type":"string","description":"Model unique identifier"},"name":{"type":"string","description":"Display name of the model"},"model":{"type":"string","description":"Model identifier used in API calls"},"provider":{"type":"string","enum":["openai","anthropic","google","azure","custom"],"description":"AI provider"},"status":{"type":"string","enum":["active","inactive"],"description":"Model availability status"},"description":{"type":"string","description":"Model description"},"maxTokens":{"type":"integer","description":"Maximum tokens supported"},"contextWindow":{"type":"integer","description":"Context window size"}}},"CustomModelsResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AIModel"}}}},"SystemModelsResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AIModel"}}}},"OnboardingQuestion":{"type":"object","properties":{"_id":{"type":"string"},"code":{"type":"string","description":"Stable code, prefixed by questionType (e.g. PERSONA_01, PERSONA_12)."},"question":{"type":"string"},"objectif":{"type":"string","description":"Long-form prompt/hint shown under the question (HTML allowed)."},"required":{"type":"boolean"},"position":{"type":"integer"},"section":{"type":"integer"},"status":{"type":"string","enum":["active","inactive","deleted"]},"mbti":{"type":"boolean"},"type":{"type":"string","enum":["text","single_choice","multi_choice","slider_group"],"default":"text","description":"Answer control type. Absent/omitted renders as plain text; single_choice/multi_choice render selectable tiles; slider_group renders 1-7 scale rows."},"config":{"type":"object","description":"Per-type authoring data. single_choice/multi_choice: { choices: [{ value, label, image? }] }. slider_group: { scales: [{ key, label, lowLabel, highLabel }] }. text: { multiline?: boolean }."},"imageUrl":{"type":"string","description":"Optional banner image shown above the question."},"created":{"type":"string","format":"date-time"}}},"InstitutionData":{"type":"object","properties":{"_id":{"type":"string","description":"Institution unique identifier","example":"6631915765bb0a94cfd6ca99"},"name":{"type":"string","description":"Institution domain name","example":"scarlet.hugo.praxis-ai.instructure.com"},"ainame":{"type":"string","description":"AI assistant name for the institution","example":"Scarlet"}}},"UserData":{"type":"object","properties":{"_id":{"type":"string","description":"User unique identifier","example":"6430d02554cd4e00403e8b05"},"email":{"type":"string","format":"email","description":"User email address","example":"hugo.lebegue@gmail.com"},"fname":{"type":"string","description":"User first name","example":"Hugo"},"lname":{"type":"string","description":"User last name","example":"Lebegue"}}},"UserSetting":{"type":"object","properties":{"_id":{"type":"string","description":"Setting unique identifier","example":"687e6ee393c79717445848de"},"key":{"type":"string","description":"Setting key name","example":"variable-2"},"value":{"type":"string","description":"Setting value","example":"variable value"},"institution":{"type":"string","description":"Institution ID this setting belongs to","example":"6631915765bb0a94cfd6ca99"},"user":{"type":"string","description":"User ID this setting belongs to","example":"6430d02554cd4e00403e8b05"},"status":{"type":"string","enum":["active","inactive"],"description":"Setting status","example":"active"},"admin_only":{"type":"boolean","description":"Whether setting is admin-only","example":true},"institution_shared":{"type":"boolean","description":"Whether setting is shared across institution","example":true},"editable_others":{"type":"boolean","description":"Whether setting can be edited by others","example":true},"created":{"type":"string","format":"date-time","description":"Setting creation timestamp"},"institution_data":{"$ref":"#/components/schemas/InstitutionData"},"user_data":{"$ref":"#/components/schemas/UserData"},"index":{"type":"integer","description":"Position index assigned during retrieval, starting at 1","example":1}}},"CreateSettingRequest":{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"Setting key name (must be unique within the institution)","example":"variable-4"},"value":{"type":"string","description":"Setting value (optional)","example":"the value"},"admin_only":{"type":"boolean","description":"Whether setting is admin-only","example":true},"editable_others":{"type":"boolean","description":"Whether setting can be edited by others","example":true},"institution_shared":{"type":"boolean","description":"Whether setting is shared across institution","example":true}}},"CreatedSetting":{"type":"object","properties":{"_id":{"type":"string","description":"Setting unique identifier","example":"687e71b293c797174458e6f2"},"key":{"type":"string","description":"Setting key name","example":"variable-4"},"value":{"type":"string","description":"Setting value","example":"the value"},"institution":{"type":"string","description":"Institution ID (auto-populated from authenticated user)","example":"6631915765bb0a94cfd6ca99"},"user":{"type":"string","description":"User ID (auto-populated from authenticated user)","example":"6430d02554cd4e00403e8b05"},"status":{"type":"string","enum":["active","inactive"],"description":"Setting status (defaults to active)","example":"active"},"admin_only":{"type":"boolean","description":"Whether setting is admin-only","example":true},"institution_shared":{"type":"boolean","description":"Whether setting is shared across institution","example":true},"editable_others":{"type":"boolean","description":"Whether setting can be edited by others","example":true},"created":{"type":"string","format":"date-time","description":"Setting creation timestamp"},"__v":{"type":"integer","description":"Mongoose version key","example":0}}},"UpdateSettingRequest":{"type":"object","description":"At least one property must be provided. Any subset of fields can be sent.","properties":{"key":{"type":"string","description":"Setting key name","example":"variable-2"},"value":{"type":"string","description":"Setting value","example":"variable value"},"status":{"type":"string","enum":["active","inactive","deleted"],"description":"Setting status","example":"active"},"admin_only":{"type":"boolean","description":"Whether setting is admin-only","example":true},"editable_others":{"type":"boolean","description":"Whether setting can be edited by others","example":true},"institution_shared":{"type":"boolean","description":"Whether setting is shared across institution","example":true}}},"GetPlanResponse":{"type":"object","properties":{"plan":{"type":"string","description":"Plan name derived from the Stripe price ID (empty string if none)","example":"pro"},"status":{"type":"string","description":"Stripe subscription status","enum":["incomplete","incomplete_expired","trialing","active","past_due","canceled","unpaid"],"example":"active"},"trial_end":{"type":"integer","nullable":true,"description":"Unix timestamp when the trial ends (null if no trial)","example":1700000000},"cancel_at_period_end":{"type":"boolean","description":"Whether the subscription is set to cancel at the end of the current period","example":false},"current_period_end":{"type":"integer","nullable":true,"description":"Unix timestamp when the current billing period ends","example":1700000000},"canceled_at":{"oneOf":[{"type":"integer"},{"type":"string"}],"nullable":true,"description":"Unix timestamp when the subscription was canceled, or empty string if not canceled"},"subscription":{"type":"boolean","description":"Whether the user has an active Stripe subscription","example":true}}},"SubscribeRequest":{"type":"object","required":["priceId"],"properties":{"priceId":{"type":"string","description":"Stripe price ID to subscribe to","example":"price_1234567890"},"trial":{"type":"string","description":"Set to 'true' to enable a trial period on the subscription","example":"true"},"mode":{"type":"string","description":"Stripe checkout mode (defaults to 'subscription')","enum":["subscription","payment"],"example":"subscription"},"institutionId":{"type":"string","description":"Institution ID to associate with the subscription (falls back to user's institution)"},"plan":{"type":"string","description":"Plan identifier"}}},"CheckoutResultResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"plan":{"type":"string","description":"Package display name (e.g., 'Silver', 'Pro')","example":"Pro"},"credits":{"type":"integer","nullable":true,"description":"Number of credits added to the account","example":500},"amount":{"type":"integer","description":"Total amount charged in cents","example":5000},"currency":{"type":"string","description":"Three-letter ISO currency code","example":"usd"},"accountName":{"type":"string","description":"Name of the account credited (institution name or 'Personal')","example":"Personal"},"paymentStatus":{"type":"string","description":"Stripe payment status","enum":["paid","unpaid","no_payment_required"],"example":"paid"},"mode":{"type":"string","description":"Stripe checkout mode","enum":["payment","subscription"],"example":"payment"}}},"RefreshProfileResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Whether the operation was successful"},"token":{"type":"string","description":"Fresh JWT token (sliding session). Each profile refresh extends the session by the configured expiration period (default 6 hours). Store this token and use it for subsequent API requests."},"profile":{"type":"object","description":"Full user profile object (User model fields merged with populated institution, plus Google OAuth flags). Sensitive fields (password, permissions, __v, created) are stripped.","properties":{"_id":{"type":"string","description":"User unique identifier"},"email":{"type":"string","description":"User email address"},"fname":{"type":"string","description":"First name"},"lname":{"type":"string","description":"Last name"},"picture":{"type":"string","description":"URL to user profile picture"},"accountType":{"type":"string","enum":["user","admin","super"],"description":"Account type: user, admin, or super"},"credits":{"type":"integer","description":"User credit balance"},"creditsUsed":{"type":"integer","description":"Total credits consumed"},"plan":{"type":"string","description":"Subscription plan (e.g. free, sdk, entry, premium, pro, monthly)"},"status":{"type":"string","description":"User account status (e.g. active, inactive, trialing, deleted)"},"customerId":{"type":"string","description":"Stripe customer ID"},"institution":{"type":"object","description":"Populated institution object with nested account. Contains public fields (name, ainame, status, credits, poolCredits, rtEnabled, rtVoice, picture, css, about, etc.) returned by instancePublicFields(). The nested `account` object includes: name, managerEmail, domainUrls, status, credits, kagFusionEnabledAllInstances, kagFusionAllowInstanceOverride, and **privacyPolicyHTML** (consent gate for the active session — kept here rather than in /api/user/refresh/entitlements account_data to avoid duplicating multi-KB HTML across rows that share the same account)."},"remember_history_count":{"type":"integer","description":"Number of history entries to remember"},"browser_voice":{"type":"string","description":"Selected browser TTS voice"},"browser_voices":{"type":"object","description":"Browser voice configuration per language"},"rt_voice":{"type":"string","description":"Selected real-time voice"},"use_location":{"type":"boolean","description":"Whether location services are enabled"},"use_stt":{"type":"boolean","description":"Whether speech-to-text is enabled"},"pin_ui":{"type":"boolean","description":"Whether UI sidebar is pinned"},"showSideBar":{"type":"boolean","description":"Whether sidebar is visible"},"galleryAsGrid":{"type":"boolean","description":"Whether gallery displays as grid"},"ragOnlySearch":{"type":"boolean","description":"Knowledge — \"Search Only\" output toggle (axis 2 of 2). When `true`\nwith retrieval on, Pria returns the raw vault chunks instead of an\nLLM-rewritten answer. Orthogonal to `ragIgnore` / `ragKagMode`.\nSee PUT /user/me for the full combination matrix.\n"},"ragIgnore":{"type":"boolean","description":"Knowledge — \"Disabled\" retrieval toggle (axis 1 of 2). When `true`,\nretrieval is skipped entirely and the LLM answers from its training\nonly. Takes precedence over `ragKagMode` and `ragOnlySearch`.\n"},"ragKagMode":{"type":"boolean","description":"Knowledge — \"RAG + KAG Fusion\" retrieval toggle (axis 1 of 2). When\n`true` with `ragIgnore=false` AND the user/institution is KAG-eligible,\nthe knowledge-graph leg runs alongside the dense vector leg and the\ntwo are merged via Reciprocal Rank Fusion. KAG is always an\naugmentation on top of RAG — there is no \"KAG without RAG\" mode.\n"},"dark_mode":{"type":"boolean","description":"Whether dark mode is enabled"},"mustChangePassword":{"type":"boolean","description":"Whether user must change password on next login"},"updatePasswordOnSSO":{"type":"boolean","description":"Whether to update password on SSO login"},"resetCodeId":{"type":"string","description":"Password reset code identifier"},"referralId":{"type":"string","description":"User referral ID"},"trial_end":{"type":"string","format":"date-time","description":"Trial period end date"},"trial_used":{"type":"boolean","description":"Whether trial has been used"},"current_period_end":{"type":"string","format":"date-time","description":"Current billing period end date"},"cancel_at_period_end":{"type":"boolean","description":"Whether subscription cancels at period end"},"lxp_user_id":{"type":"string","description":"LXP platform user ID"},"lxp_partner_name":{"type":"string","description":"LXP partner name"},"lxp_role_name":{"type":"string","description":"LXP role name"},"googleLoginToken":{"type":"object","description":"Google OAuth token object (conditionally included). Omitted when institution uses institution-level Google account."},"googleOAuthScopes":{"type":"array","items":{"type":"string"},"description":"Google OAuth scopes the user has authorized. Omitted when institution uses institution-level Google account."},"institutionGoogleOAuthEnabled":{"type":"boolean","description":"Whether the current institution has Google OAuth enabled"},"institutionGoogleOAuthScopes":{"type":"array","items":{"type":"string"},"description":"Google OAuth scopes configured at institution level"},"institutionGoogleWorkspaceEnabled":{"type":"boolean","description":"Whether Google Workspace is enabled for users at the institution"},"institutionGoogleUseInstitutionAccount":{"type":"boolean","description":"Whether the institution uses a shared Google account (hides user-level tokens when true)"}}}}},"EntitlementData":{"type":"object","description":"A user-institution membership record. Fields digitaltwin, creditAwarded, and lastIP are always stripped from the response.","properties":{"_id":{"type":"string","description":"Unique identifier for the user-institution record"},"entitlements":{"type":"array","items":{"type":"string"},"description":"Array of permission strings (e.g. institutions.list, users.add)"},"accountType":{"type":"string","enum":["super","admin","user"],"description":"Role in this institution: super, admin, or user"},"status":{"type":"string","enum":["active","inactive","pending","deleted"],"description":"Current status of the membership (deleted records are filtered out in the query, but the enum value exists)"},"user":{"type":"string","description":"User ID associated with this entitlement"},"institution":{"type":"string","description":"Institution ObjectId (raw ID after transformation, or undefined for personal accounts)"},"institution_data":{"type":"object","description":"Populated institution object with selected fields: name, ainame, status, credits, poolCredits, rtEnabled, rtAdminOnly, rtVoice, displayAgentDetails, displayThinkingDetails, displayRagSearchDetails, displayThinkingExecution, displayToolExecution, disableAddCredits, contactEmail, account (ID), picture, picture_animated, questionType, allowJoining, joiningAdminOnly, joinId, publicId, preventSwitchingInstance. NOTE: `css`, `theme`, and `about` are intentionally omitted to keep the picker payload small. css/theme apply only to the active session (loaded via /api/user/refresh/profile → instancePublicFields). about is lazy-loaded per institution via POST /api/user/institutionAbout when the Gallery card flips or the About modal opens.","properties":{"_id":{"type":"string"},"name":{"type":"string","description":"Institution display name"},"ainame":{"type":"string","description":"AI assistant name for this institution"}}},"account_data":{"type":"object","description":"Populated account object with selected fields: name, credits, managerEmail, status, transferCreditsMode. NOTE: `privacyPolicyHTML` is intentionally omitted — the consent gate only renders for the ACTIVE userInstitution, so the field has been moved onto profile.institution.account (returned by /api/user/refresh/profile) to avoid duplicating multi-KB HTML across every entitlement row sharing the same account.","properties":{"_id":{"type":"string"},"name":{"type":"string","description":"Account name"}}},"created":{"type":"string","format":"date-time","description":"Creation timestamp"},"lastLogin":{"type":"string","format":"date-time","description":"Last login timestamp"},"lastKA":{"type":"string","format":"date-time","description":"Last keep-alive timestamp"},"favorite":{"type":"boolean","description":"Whether this membership is marked as favorite (affects sort order)"},"favoriteAssistants":{"type":"array","items":{"type":"string"},"description":"Array of favorite assistant ObjectIds"},"acceptedAccountPrivacyPolicyDate":{"type":"string","format":"date-time","description":"Date when user accepted the account privacy policy"},"creditAwardedDate":{"type":"string","format":"date-time","description":"Date when credits were awarded to this membership"},"lticontextid":{"type":"string","description":"LTI context ID linked to this user-institution"},"lastConversation":{"$ref":"#/components/schemas/LastConversation"},"canvasApiToken":{"$ref":"#/components/schemas/CanvasApiToken"},"digitaltwinrole":{"type":"string","description":"Digital twin role identifier"},"joinedAt":{"type":"string","format":"date-time","description":"Date when the user joined this institution"}}},"AccountManagerAccount":{"type":"object","properties":{"_id":{"type":"string","description":"Account ObjectId"},"name":{"type":"string","description":"Account name"}}},"RefreshEntitlementsResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the operation was successful"},"entitlements":{"type":"array","items":{"$ref":"#/components/schemas/EntitlementData"},"description":"Array of user-institution records, sorted by favorite (desc), ainame (asc), institution name (asc). Null-institution duplicates are auto-cleaned."},"accountManagerAccounts":{"type":"array","items":{"$ref":"#/components/schemas/AccountManagerAccount"},"description":"Accounts where this user is an account manager (array of {_id, name} objects)"}}},"UserInstitutionSwitchResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the institution switch was successful"},"message":{"type":"string","description":"Success message confirming the profile switch"}}},"CanvasUser":{"type":"object","properties":{"id":{"type":"integer","description":"Canvas user ID","example":110},"name":{"type":"string","description":"Full name of the user","example":"Hugo Lebegue"},"global_id":{"type":"string","description":"Global Canvas user identifier","example":"243790000000000110"},"effective_locale":{"type":"string","description":"User's effective locale setting","example":"en"},"fake_student":{"type":"boolean","description":"Whether the user is a fake student account","example":false}}},"CanvasApiToken":{"type":"object","required":["access_token","token_type","user","institutionid"],"properties":{"access_token":{"type":"string","description":"Canvas API access token","example":"24379~vCERVnFWh9yyEVQkafGWQL8WwUk9YKv2mEmMyv6WtkzUYkEHDRGtPP2aMMCYf4C9"},"token_type":{"type":"string","description":"Type of the token","example":"Bearer"},"user":{"$ref":"#/components/schemas/CanvasUser"},"canvas_region":{"type":"string","description":"Canvas region identifier","example":"us-east-1"},"expires_in":{"type":"integer","description":"Token expiration time in seconds","example":3600},"created":{"type":"integer","description":"Token creation timestamp","example":1},"refresh_token":{"type":"string","description":"Refresh token for renewing access","example":"24379~MTDRu7Aw3RGk2DPmrNuAP2E37LcRHQ2A2wk978CMK3GTMFwkrJA8wQkeaKmxWvRA"},"institutionid":{"type":"string","description":"Institution identifier","example":"65cfebc32f5e1b37d4e52329"}}},"LastConversation":{"type":"object","properties":{"course_id":{"type":"number","description":"Unique identifier for the course"},"course_name":{"type":"string","description":"Name of the course/conversation"},"assistant":{"type":"object","properties":{"_id":{"type":"string","description":"Assistant unique identifier"},"name":{"type":"string","description":"Assistant name"},"picture_url":{"type":"string","description":"URL to assistant picture"}}},"history_count":{"type":"number","description":"Number of dialogue entries in history"},"last_dialogue_date":{"type":"string","format":"date-time","description":"Timestamp of last dialogue"}}},"UserInstitutionUpdateRequest":{"type":"object","description":"At least one field is required. If no valid fields are provided, a 400 error is returned.","properties":{"lastConversation":{"$ref":"#/components/schemas/LastConversation"},"canvasApiToken":{"oneOf":[{"$ref":"#/components/schemas/CanvasApiToken"},{"type":"string","enum":[""],"description":"Send empty string to remove the canvas token"}]},"favoriteAssistants":{"type":"array","items":{"type":"string"},"description":"Array of assistant ObjectIds to mark as favorites"},"acceptedAccountPrivacyPolicyDate":{"oneOf":[{"type":"string","format":"date-time"},{"type":"string","enum":[""],"description":"Send empty string to clear the accepted date"}],"description":"Date when user accepted the account privacy policy"},"favorite":{"type":"boolean","description":"Whether to mark this institution as a favorite"}}},"UserInstitutionUpdateResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Operation success status","example":true},"message":{"type":"string","description":"Success message","example":"userInstitution preference saved sucessfully"}}},"ToolItem":{"type":"object","required":["_id","name","status","description","rtEnabled","rtOnly"],"properties":{"_id":{"type":"string","description":"Tool unique identifier","example":"6631915765bb0a94cfd6ca99"},"name":{"type":"string","description":"Tool name/identifier","example":"call_google_calendar"},"status":{"type":"string","enum":["active"],"description":"Tool status. Only active tools are returned; deleted and inactive tools are excluded.","example":"active"},"description":{"type":"string","description":"Tool description","example":"Access Google Calendar to manage events and schedules"},"instructions":{"type":"string","description":"Detailed usage instructions for the tool, intended for inclusion in an LLM system prompt. Omitted when the request body sets `minimum: true`.\n","example":"Use 'call_google_calendar' to create and retrieve events..."},"rtEnabled":{"type":"boolean","description":"Whether real-time mode is enabled for this tool","example":true},"rtOnly":{"type":"boolean","description":"Whether tool only works in real-time mode","example":false},"unavailable":{"type":"boolean","description":"Whether the tool is currently unavailable for the authenticated user. This field is only present on cloud-integrated Google tools (e.g. call_google_gmail, call_google_drive, call_google_calendar, call_google_sheets, call_google_docs, call_google_slides, call_google_meet, call_google_maps, call_google_classroom). Non-Google tools will not include this field.\n","example":false},"unavailableReason":{"type":"string","description":"Human-readable reason why a Google tool is unavailable. Only present on cloud-integrated Google tools. Empty string when the tool is available.\n","enum":["","Google OAuth not configured","Google Cloud not enabled for users","Google Cloud not connected for this instance"],"example":""}}},"GetToolsResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","description":"Whether the operation was successful","example":true},"data":{"type":"array","items":{"$ref":"#/components/schemas/ToolItem"},"description":"List of tools sorted in ascending order by name"},"message":{"type":"string","description":"Present only when no tools are found, with the value \"No Tools\"","example":"No Tools"}}},"GetToolsErrorResponse":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean","description":"Always false for error responses","example":false},"error":{"type":"object","description":"The raw error object from the failed operation"},"message":{"type":"string","description":"Error message in the format \"Error getting tools <details>\"","example":"Error getting tools Database connection failed"}}},"TranscribeResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"transcript":{"type":"string","description":"The recognised text. Empty string on silent input."},"provider":{"type":"string","description":"Resolved STT provider id (one of `mistral_cli`, `openai_cli`, `bedrock_cli`, `anthropic_cli`, `google_genai_cli`, `xai_cli`)."},"durationMs":{"type":"integer","nullable":true,"description":"Audio duration in milliseconds when the provider returns it (openai). Null when not reported (mistral)."}}},"UploadFile":{"type":"object","properties":{"_id":{"type":"string","description":"Unique identifier for the file"},"filename":{"type":"string","description":"System-generated filename (omitted when lean: true)"},"originalname":{"type":"string","description":"Original filename as uploaded, primary name identifier for the user and LLM"},"mimetype":{"type":"string","description":"MIME type of the file (omitted when lean: true)"},"created":{"type":"string","format":"date-time","description":"File creation timestamp"},"filesize":{"type":"integer","description":"File size in bytes"},"status":{"type":"string","enum":["inactive","selected","active","error","deleted"],"description":"Persisted upload state. inactive: waiting on RAG / ingestion queue. selected: RAG-ingested and included in the vault. active: uploaded with skipIndexing — visible but not RAG-indexed. error: RAG/ingestion failed. deleted: soft-delete tombstone."},"thumbnail":{"type":"string","description":"Thumbnail picture encoded (omitted when compact: true)"},"user":{"type":"string","description":"User ID who owns the file"},"institution":{"type":"string","description":"Institution ID the file is shared with (omitted when null)"},"account_shared":{"type":"boolean","description":"Whether the file is shared across all institutions in the same account"},"file_summary":{"type":"string","description":"AI-generated summary of file content (omitted when compact: true or lean: true)"},"file_title":{"type":"string","description":"AI-generated title for the file (omitted when compact: true)"},"file_url":{"type":"string","format":"uri","description":"URL to access the file"},"tokens_used":{"type":"integer","description":"Number of tokens used for processing"},"is_public":{"type":"boolean","description":"Whether the file is publicly accessible (anyone with the link can access without authentication). Default: false.","default":false},"is_private":{"type":"boolean","description":"Whether the file is confidential (owner-only access, won't appear in citations). Default: false. Cannot be true when is_public is true.","default":false},"file_dimensions":{"type":"string","description":"Image dimensions (for image files)"},"file_authors":{"type":"string","description":"Authors of the content (omitted when lean: true)"},"embeddings_model":{"type":"string","description":"Model used to generate embeddings"},"summary_model":{"type":"string","description":"Model used to generate the file summary"},"image_analysis_model":{"type":"string","description":"Model used for image analysis"},"googleDriveFileId":{"type":"string","description":"Google Drive file ID if synced from Drive"},"googleDriveModifiedTime":{"type":"string","format":"date-time","description":"Last modified time from Google Drive"},"ragHitCount":{"type":"integer","description":"Number of times this file appeared in RAG search results (deduplicated per search)","default":0},"lastRagHitAt":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of most recent RAG search match (null if never used)"},"vaultHealthScore":{"type":"integer","nullable":true,"description":"Percentage (0-100) of chunks flagged as unoptimized content (code, markup, structured data). null = not yet scanned."},"fileOnDisk":{"type":"boolean","description":"Whether the physical file exists on disk. Only present when the upload has a file_url."},"owner_data":{"type":"object","description":"Trimmed owner info. In full mode, populated via $lookup. In lean/compact mode, batch-resolved for instance and account vault files. Omitted for personal vault files.","properties":{"email":{"type":"string","format":"email"},"fname":{"type":"string"},"lname":{"type":"string"},"institution":{"type":"string","description":"Owner's institution ID (only present in full $lookup mode)"}}},"institution_data":{"type":"object","description":"Institution info from $lookup (omitted when compact: true or lean: true)","properties":{"name":{"type":"string"},"ainame":{"type":"string"}}},"institution_name":{"type":"string","description":"Display name of the source institution (ainame or name). Only present for account_shared files with an institution."},"institution_display_name":{"type":"string","description":"Full institution name, shown when it differs from institution_name (the ainame). Only present for account_shared files where ainame !== name."},"index":{"type":"integer","description":"1-indexed position of this item within the returned page"},"collection":{"type":"string","description":"ObjectId of the collection this file belongs to, or omitted when the file lives at the vault root."},"collection_path":{"type":"array","description":"Server-stamped breadcrumb from root → leaf of the file's containing collection (only when the file is inside a collection). Used by the Files panel to render `Collection 1 › sub-collection 2 › file.txt` without an extra round-trip. Walks `collection.parent` to bounded depth-10 (DocumentDB-friendly — no $graphLookup).\n","items":{"type":"object","properties":{"_id":{"type":"string","description":"Collection ObjectId"},"name":{"type":"string","description":"Collection display name"},"color":{"type":"string","nullable":true,"description":"Per-collection accent hex (`#RGB` or `#RRGGBB`) for the folder icon, or null for the default outline icon."}}}}}},"UploadSearchRequest":{"type":"object","properties":{"lean":{"type":"boolean","description":"When true, omits file_summary, filename, file_authors, mimetype, owner_data, and institution_data"},"compact":{"type":"boolean","description":"When true, omits file_title, file_summary, thumbnail, owner_data, and institution_data"},"status":{"type":"string","enum":["inactive","selected","active","error","deleted","excluded","processing"],"description":"Filter by status. Real DB values (inactive/selected/active/error/deleted) match exactly.\nPseudo-values:\n  - `excluded`: expands to `status $nin:['selected','deleted']`\n  - `processing`: matches `ingestion.phase ∈ {extract, chunk, sanitize, embed, kag}` AND `status $ne:'deleted'`. Use this to surface files currently mid-ingestion regardless of their literal status (in-flight files usually carry status:'inactive' until the pipeline finishes).\nWhen omitted, defaults to $ne:'deleted'.\n"},"fileNameSearch":{"type":"string","description":"Case-insensitive regex search term applied to originalname"},"uploadId":{"type":"string","description":"Return only the upload with this specific ID"},"uploadIds":{"type":"array","items":{"type":"string"},"maxItems":50,"description":"Batch fetch — return only uploads whose _id is in this list. Capped to 50. Used by the UI to poll in-flight ingestion uploads without re-running the full list query."},"vault":{"type":"string","enum":["personal","instance","account"],"description":"Filter by vault type. personal: user-owned files with no institution. instance: institution files that are not account-shared. account: account-shared files across all institutions."},"institution":{"type":"string","description":"Institution ID to scope the query. Used with vault or all to target a specific institution."},"page":{"type":"integer","minimum":1,"default":1,"description":"Page number for pagination (1-based)"},"pageSize":{"type":"integer","minimum":1,"maximum":50,"default":25,"description":"Number of items per page (clamped to 1-50)"},"sortAscending":{"type":"boolean","default":false,"description":"When true, sorts in ascending order; otherwise descending"},"nameOrder":{"type":"boolean","description":"When truthy, sorts by created date instead of the default case-insensitive originalname sort"},"sortBy":{"type":"string","enum":["ragHitCount","lastRagHitAt"],"description":"Sort field. When provided, overrides nameOrder. ragHitCount sorts by usage count, lastRagHitAt sorts by recency of RAG matches (nulls first when ascending)."},"includeCollectionFiles":{"type":"boolean","description":"When true, includes files that belong to a vault-scoped collection in the results.\nDefault false — files in collections are excluded from root vault listings to avoid\nduplicates with the CollectionStrip render. Auto-bypassed (treated as true) for the\nattention filters `status: 'processing'` and `status: 'error'` so the user can see\nevery file needing attention from the vault root, regardless of its container.\n"},"includeCounts":{"type":"boolean","description":"When true, appends a vaultCounts object to the response with total and active counts per vault (personal, instance, account)"}}},"UploadSearchResponse":{"type":"object","required":["success","data","total","hasMore","page","pageSize"],"properties":{"success":{"type":"boolean","description":"Whether the request succeeded"},"data":{"type":"array","items":{"$ref":"#/components/schemas/UploadFile"},"description":"Array of upload records for the requested page"},"total":{"type":"integer","description":"Total number of matching uploads across all pages"},"hasMore":{"type":"boolean","description":"Whether more pages exist beyond the current page"},"page":{"type":"integer","description":"Current page number (mirrors the request value)"},"pageSize":{"type":"integer","description":"Items per page used (mirrors the resolved request value)"},"vaultCounts":{"type":"object","description":"Vault-level counts (only present when includeCounts: true)","properties":{"personal":{"type":"object","properties":{"total":{"type":"integer","description":"Total non-deleted personal files"},"active":{"type":"integer","description":"Personal files with status selected"}}},"instance":{"type":"object","properties":{"total":{"type":"integer","description":"Total non-deleted instance files"},"active":{"type":"integer","description":"Instance files with status selected"}}},"account":{"type":"object","properties":{"total":{"type":"integer","description":"Total non-deleted account-shared files"},"active":{"type":"integer","description":"Account-shared files with status selected"}}}}}}},"UpdateUploadRequest":{"type":"object","description":"Only whitelisted fields are accepted. Fields not in the allowed list are silently ignored. Setting is_public to true automatically forces is_private to false.","properties":{"originalname":{"type":"string","description":"The original name of the uploaded file","example":"markdown_render_with_error_boundary"},"status":{"type":"string","description":"The new status for the upload. active: default status. selected: file will be included in RAG searches. deleted: soft deleted, hidden from lists and RAG.","enum":["active","selected","deleted"],"example":"selected"},"is_public":{"type":"boolean","description":"When true, anyone with the link can access the file without authentication. Setting to true automatically clears is_private.","example":false},"is_private":{"type":"boolean","description":"When true, marks the file as confidential (owner-only access, hidden from citations). Cannot be true when is_public is true.","example":true},"institution":{"type":"string","description":"Institution ID to share the file with. Send an empty string \"\" or null to unset (remove) the institution field via $unset."},"file_title":{"type":"string","description":"Title for the file"},"file_summary":{"type":"string","description":"Summary of the file content"},"file_authors":{"type":"string","description":"Authors of the content"},"account_shared":{"type":"boolean","description":"Whether the file is shared across the institution account"},"collection":{"type":"string","description":"Collection ID. Send an empty string \"\" or null to remove from collection."}}},"UpdateUploadResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates if the operation was successful","example":true},"message":{"type":"string","description":"Success message","example":"Upload updated!"}}},"AgentWorkspaceRuntime":{"type":"object","properties":{"provider":{"type":"string","example":"ecs-fargate"},"cluster":{"type":"string","example":"pria-ecs"},"platformName":{"type":"string","example":"pria-agents-platform"},"workspaceId":{"type":"string"},"taskArn":{"type":"string"},"taskDefinitionArn":{"type":"string"},"efsAccessPointId":{"type":"string"}}},"AgentWorkspaceStatus":{"type":"object","properties":{"status":{"type":"string","enum":["requested","provisioning","ready","failed","stopped"]},"externalId":{"type":"string"},"desktopPath":{"type":"string","description":"Relative same-origin desktop path. ECS mode returns /api/agents/workspace/proxy/<workspaceId>/ when ready; stub mode may return a placeholder path for local UI tests."},"runtime":{"$ref":"#/components/schemas/AgentWorkspaceRuntime"},"lastHeartbeatAt":{"type":"string","format":"date-time","nullable":true}}},"AgentWorkspacePreferences":{"type":"object","description":"Per-institution agent workspace resource preferences.","properties":{"cpu":{"type":"number","minimum":0.25,"maximum":4,"default":1,"description":"Allocated vCPUs."},"memoryMb":{"type":"integer","minimum":512,"maximum":8192,"default":2048,"description":"Allocated memory in MB."},"storageGb":{"type":"integer","minimum":5,"maximum":100,"default":20,"description":"Allocated storage in GB."},"gui":{"type":"boolean","default":true,"description":"Whether the workspace runs a GUI desktop."},"vncResolution":{"type":"string","default":"1280x800","description":"Desktop resolution for the VNC session (e.g. \"1280x800\")."}}},"AgentWorkspaceMeResponse":{"type":"object","properties":{"success":{"type":"boolean"},"agent":{"type":"object","properties":{"enabled":{"type":"boolean"},"status":{"type":"string","enum":["not_enabled","requested","provisioning","ready","failed","disabled"]},"preset":{"type":"string","enum":["default","developer","browser-worker"]},"preferences":{"$ref":"#/components/schemas/AgentWorkspacePreferences"},"requestedAt":{"type":"string","format":"date-time"},"provisionedAt":{"type":"string","format":"date-time"},"lastError":{"type":"string"},"workspace":{"allOf":[{"$ref":"#/components/schemas/AgentWorkspaceStatus"}],"nullable":true}}}}},"AgentWorkspaceTicketRequest":{"type":"object","required":["workspaceId"],"properties":{"workspaceId":{"type":"string","description":"Caller's own workspace runtime id (matches\n`agent.workspace.runtime.workspaceId` returned by\n`GET /api/user/agents/workspace/me`). The endpoint only issues\ntickets for a workspace owned by `req.user._id` and in\n`status: 'ready'`.\n","example":"pria-ws-7a3f9"}}},"AgentWorkspaceTicketResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"workspaceId":{"type":"string","description":"Echo of the validated workspace id."},"vncUrl":{"type":"string","description":"Fully-qualified WebSocket URL (ws:// or wss://, matching the\ncurrent request scheme) that the browser opens to reach the\nKasmVNC desktop. Includes the single-use `ticket` query param.\n","example":"wss://pria.praxislxp.com/api/agents/workspace/vnc/pria-ws-7a3f9?ticket=eyJhbGciOi..."},"vncPassword":{"type":"string","description":"RFB-protocol password required by KasmVNC during the WebSocket\nhandshake. This is **in addition** to the HTTP Basic auth\nhandled server-side by the proxy. The browser never persists it\n— single use per RFB handshake for the lifetime of the ticket.\n"},"expiresInSec":{"type":"integer","description":"Ticket TTL in seconds (currently fixed at 60).","example":60}}},"RTSessionRequest":{"type":"object","description":"Provider, model, and voice are NOT accepted from the request — they\nare resolved server-side from the user's `institution.rtModel`\nsetting. The client only needs to send `requestArgs` carrying the\nrealtime context (assistant, course, user clock) so the server can\nbuild the right system prompt. An optional top-level `token` is\naccepted as an alternative to the `Authorization` /\n`x-access-token` headers for browser clients that cannot set\ncustom headers.\n","properties":{"token":{"type":"string","description":"Optional auth JWT — alternative to `Authorization` / `x-access-token` headers."},"requestArgs":{"type":"object","description":"Realtime context bag merged into the user object before context build.","properties":{"assistantId":{"type":"string","description":"ObjectId of the active assistant; influences the system prompt."},"selectedCourse":{"type":"object","description":"Currently selected conversation/course (course_id, course_name, assistant, etc.)."},"userISODate":{"type":"string","format":"date-time","description":"Client clock at request time (drives \"now\" in the system prompt)."},"userTimezone":{"type":"string","description":"IANA timezone (e.g. `America/New_York`)."},"socketId":{"type":"string","description":"Optional Socket.IO id for streaming side-channel events."}}}}},"RTSessionResponse":{"oneOf":[{"type":"object","description":"OpenAI/Gemini/xAI realtime session response","properties":{"provider":{"type":"string"},"sessionId":{"type":"string","description":"Realtime session identifier"},"ephemeralKey":{"type":"string","description":"Ephemeral key for WebRTC connection"},"model":{"type":"string","description":"AI model being used"}}},{"type":"object","description":"LemonSlice Hosted Pipeline Daily room response","properties":{"provider":{"type":"string","example":"lemonslice"},"pipeline":{"type":"string","example":"hosted"},"daily_room_url":{"type":"string","example":"https://lemonslice.daily.co/abc123"},"daily_token":{"type":"string"},"session_id":{"type":"string"},"image_url":{"type":"string"},"allow_imagine":{"type":"boolean"}}},{"type":"object","description":"Anam Pria-direct Custom LLM (CUSTOMER_CLIENT_V1) session response","properties":{"provider":{"type":"string","example":"anam_pria_custom_llm"},"adapterMode":{"type":"string","example":"anam-client-custom-llm"},"llmId":{"type":"string","example":"CUSTOMER_CLIENT_V1"},"anam_session_token":{"type":"string","description":"Short-lived Anam session token (browser-safe; no API key)"},"anam_avatar_id":{"type":"string"},"image_url":{"type":"string"},"loading_video_url":{"type":"string"},"intro_message":{"type":"string"},"capabilities":{"type":"object"},"audio_metadata":{"type":"object"}}}]},"RTToolExecRequest":{"type":"object","required":["toolName","arguments"],"properties":{"toolName":{"type":"string","description":"Name of the tool to execute","example":"search_rag"},"arguments":{"type":"object","description":"Tool-specific arguments"},"callId":{"type":"string","description":"Unique call identifier for tracking"},"sessionId":{"type":"string","description":"Realtime session ID"}}},"RTToolExecResponse":{"type":"object","properties":{"success":{"type":"boolean"},"callId":{"type":"string","description":"Call ID for correlation"},"result":{"type":"object","description":"Tool execution result"},"error":{"type":"string","description":"Error message if execution failed"}}},"RTSaveConversationRequest":{"type":"object","properties":{"sessionId":{"type":"string","description":"Realtime session ID"},"inputs":{"type":"array","items":{"type":"string"},"description":"User inputs from the session"},"outputs":{"type":"array","items":{"type":"string"},"description":"AI outputs from the session"},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"success":{"type":"boolean"},"responseDurationMs":{"type":"number"}}},"description":"Tools used during the session"},"usage":{"type":"object","properties":{"input_tokens":{"type":"integer"},"output_tokens":{"type":"integer"}},"description":"Token usage for the session"}}},"RTSaveConversationResponse":{"type":"object","properties":{"success":{"type":"boolean"},"historyId":{"type":"string","description":"ID of the saved history record"}}},"LemonsliceChatRequest":{"type":"object","properties":{"input":{"type":"string","description":"Final user utterance for this turn. Either `input` or `message` is required.","example":"What is on my schedule today?"},"message":{"type":"string","description":"Alias for `input` (legacy). One of the two must be a non-empty string."},"messages":{"type":"array","description":"Optional OpenAI-style conversation history for the turn. When omitted the server constructs `[{role:'user', content: input}]`.","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}}}},"requestArgs":{"type":"object","description":"Optional realtime context (selectedCourse, assistantId, userISODate, userTimezone, etc.). Merged into the user object before context build."},"token":{"type":"string","description":"Optional auth JWT (alternative to `Authorization` / `x-access-token` headers). Used by streaming clients that cannot easily set custom headers."}}},"LemonsliceChatSegmentLine":{"type":"object","description":"One NDJSON segment line emitted while the LLM streams. Boundary-flushed at tool calls so the avatar can narrate before/after each tool invocation.","properties":{"type":{"type":"string","enum":["segment"]},"kind":{"type":"string","enum":["speak"]},"text":{"type":"string","description":"Narration text for this segment."}}},"LemonsliceChatEndLine":{"type":"object","description":"Final NDJSON line of the turn — aggregate text, token usage, and tool invocation summary.","properties":{"type":{"type":"string","enum":["end"]},"text":{"type":"string","description":"Full concatenated narration text (sum of all `segment` lines)."},"usage":{"type":"number","description":"Prompt tokens consumed by the turn."},"cached":{"type":"number","description":"Cached prompt tokens (subset of `usage`)."},"completion":{"type":"number","description":"Completion tokens generated by the turn."},"tools":{"type":"array","description":"Tools invoked during the turn (name, success, latency).","items":{"type":"object"}}}},"DeepgramSttSessionRequest":{"type":"object","description":"All fields optional. The endpoint only mines `requestArgs` for\nkeyterm hints — anything else is ignored.\n","properties":{"requestArgs":{"type":"object","description":"Realtime context echoed from the client. Used to derive Deepgram keyterm hints (assistant name, conversation title, explicit `keyterms[]`).","properties":{"selectedCourse":{"type":"object","properties":{"course":{"type":"object","properties":{"title":{"type":"string","description":"Used as a keyterm boost."}}},"assistant":{"type":"object","properties":{"name":{"type":"string","description":"Used as a keyterm boost."}}}}},"assistantName":{"type":"string","description":"Explicit assistant name override (used when `selectedCourse.assistant.name` is not set)."},"courseTitle":{"type":"string","description":"Explicit course title override."},"keyterms":{"type":"array","items":{"type":"string"},"description":"Additional keyterms merged with derived ones and the institution name/ainame."}}}}},"DeepgramSttSessionResponse":{"type":"object","properties":{"provider":{"type":"string","enum":["deepgram"]},"access_token":{"type":"string","description":"Short-lived Deepgram bearer token. The browser passes this via the WebSocket subprotocol when opening the WSS stream."},"expires_in":{"type":"number","description":"Token TTL in seconds. `0` in dev raw-key mode."},"wss_url":{"type":"string","description":"Deepgram listen WSS URL the client should connect to.","example":"wss://api.deepgram.com/v1/listen"},"model":{"type":"string","description":"Deepgram STT model in use (institution-tuned)."},"eot_threshold":{"type":"number","description":"End-of-turn confidence threshold."},"eot_timeout_ms":{"type":"number","description":"End-of-turn timeout in ms."},"encoding":{"type":"string","enum":["linear16"],"description":"Fixed PCM encoding the client AudioWorklet emits."},"sample_rate":{"type":"number","enum":[16000],"description":"Fixed sample rate (16 kHz mono int16)."},"keyterms":{"type":"array","items":{"type":"string"},"description":"Resolved keyterm list — institution name/ainame plus the derived/explicit terms."}}},"RequestArgs":{"type":"object","description":"Context arguments for AI conversation requests","properties":{"userISODate":{"type":"string","format":"date-time","description":"User's current timestamp in ISO 8601 format","example":"2025-06-23T06:34:24.754Z"},"userTimezone":{"type":"string","description":"User's IANA timezone identifier","example":"America/New_York"},"userGPSCoordinates":{"$ref":"#/components/schemas/GPSCoordinates"},"socketId":{"type":"string","description":"Socket.IO session ID for real-time streaming (required for Socket.IO mode)","example":"DhXE7OVjCtfUmDFTAAAB"},"selectedCourse":{"$ref":"#/components/schemas/ConversationContext"},"ragOnly":{"type":"boolean","description":"Knowledge \"Search Only\" mode. When true (with retrieval on), Pria\nreturns raw vault chunks instead of an LLM-rewritten answer.\nOrthogonal to `ragIgnore` / `kagMode`. Persisted as `ragOnlySearch`\non the user profile.\n","default":false},"ragIgnore":{"type":"boolean","description":"Knowledge \"Disabled\" mode. When true, retrieval is skipped entirely\nand the LLM answers from its training only. Takes precedence over\n`kagMode` and `ragOnly`.\n","default":false},"kagMode":{"type":"boolean","description":"Knowledge \"RAG + KAG Fusion\" mode. When true with `ragIgnore=false`\nAND the user/institution is KAG-eligible, the knowledge-graph leg\nruns alongside the dense vector leg (merged via Reciprocal Rank\nFusion). KAG is always an augmentation on top of RAG — there is\nno \"KAG without RAG\" mode. Sent on requestArgs; persisted as\n`ragKagMode` on the user profile.\n","default":false},"institutionPublicId":{"type":"string","format":"uuid","description":"Specifies the digital twin (institution) to send the command to.\nThe server validates that the authenticated user has a valid membership\nin the specified institution. When found, the user's active institution\nis switched to this institution in their profile data for the duration\nof the request.\n","example":"f831501f-b645-481a-9cbb-331509aaf8c1"},"assistantId":{"type":"string","description":"MongoDB ObjectId of the assistant to use for this request.\nTakes priority over selectedCourse.assistant._id (legacy).\nIf not provided, the assistant is resolved from selectedCourse or\nthe most recent history record for the conversation.\n","example":"6856fa89cbafcff8d98680f5"}}},"QandARequest":{"type":"object","description":"Request payload for AI Q&A conversation","required":["inputs"],"properties":{"id":{"type":"number","description":"Client-generated request identifier (epoch timestamp)","example":1750660464754},"requestArgs":{"$ref":"#/components/schemas/RequestArgs"},"inputs":{"type":"array","items":{"type":"string"},"description":"User messages to send to the AI","example":["What is machine learning?"]},"outputs":{"type":"array","items":{"type":"string"},"description":"Previous AI responses (for context continuity)","example":[]}}},"QandAResponse":{"type":"object","description":"Response from AI Q&A conversation","properties":{"success":{"type":"boolean","description":"Whether the request completed successfully"},"streamingFailed":{"type":"boolean","description":"True if Socket.IO streaming failed (response still contains full output)"},"streamingError":{"type":"string","description":"Error message if streaming failed"},"outputs":{"type":"array","items":{"type":"string"},"description":"AI-generated response messages"},"usage":{"type":"number","description":"Total tokens consumed (input + output)"},"credits":{"type":"number","description":"Credits consumed for this request"},"creditsUsed":{"type":"number","description":"User's total credits consumed"},"totalCredits":{"type":"number","description":"User's remaining credit balance"},"query_duration_ms":{"type":"number","description":"Total processing time in milliseconds"},"model":{"type":"string","description":"AI model identifier used for generation","example":"gpt-4o"}}},"SSEStreamEvent":{"type":"object","description":"Server-Sent Event payload structure for HTTP streaming.\nEach event is a JSON object sent as `data: {json}\\n\\n`.\nThe `type` field determines which other fields are present.\n","properties":{"type":{"type":"string","enum":["connected","stream","tool_call","tool_result","complete","error","done"],"description":"Event type indicator:\n- `connected`: Stream established successfully\n- `stream`: AI-generated text chunk (cumulative + delta)\n- `tool_call`: Tool/function invocation started (RAG, web search, etc.)\n- `tool_result`: Tool/function execution completed with results\n- `complete`: Final response with usage metrics and full output\n- `error`: Error occurred during processing\n- `done`: Stream terminated — no more events will follow\n"}}},"SSEConnectedEvent":{"type":"object","description":"Sent immediately after SSE connection is established","properties":{"type":{"type":"string","enum":["connected"]},"message":{"type":"string","description":"Connection status message","example":"Stream connected"}}},"SSEStreamChunkEvent":{"type":"object","description":"AI-generated text chunk, sent incrementally as the model generates output","properties":{"type":{"type":"string","enum":["stream"]},"prompt":{"type":"string","description":"Cumulative text generated so far (full response up to this point)"},"delta":{"type":"string","description":"Incremental text chunk (only the new characters since the last event)"}}},"SSEToolCallEvent":{"type":"object","description":"Emitted when the AI invokes a tool (e.g., RAG search, web search, calendar lookup)","properties":{"type":{"type":"string","enum":["tool_call"]},"call_id":{"type":"string","description":"Unique identifier for this tool invocation"},"name":{"type":"string","description":"Tool/function name being invoked","example":"search_uploads"},"arguments":{"type":"object","nullable":true,"description":"Tool arguments (null when streaming has not yet completed argument parsing)"},"displayInfo":{"type":"object","description":"UI display metadata for the tool invocation","properties":{"icon":{"type":"string","description":"Icon identifier for the tool"},"label":{"type":"string","description":"Human-readable label"},"description":{"type":"string","description":"Brief description of what the tool does"},"targetParam":{"type":"string","nullable":true,"description":"Parameter name to highlight in the UI"}}}}},"SSEToolResultEvent":{"type":"object","description":"Emitted after a tool invocation completes with results","properties":{"type":{"type":"string","enum":["tool_result"]},"call_id":{"type":"string","description":"Matches the call_id from the corresponding tool_call event"},"name":{"type":"string","description":"Tool/function name that was invoked"},"arguments":{"type":"object","description":"Tool arguments that were used"},"response":{"type":"string","description":"Tool execution response text"},"responseLength":{"type":"number","description":"Length of the response string"},"responseDurationMs":{"type":"number","description":"Tool execution time in milliseconds"},"success":{"type":"boolean","description":"Whether the tool executed successfully"}}},"SSECompleteEvent":{"type":"object","description":"Final event containing the full response and usage metrics","properties":{"type":{"type":"string","enum":["complete"]},"success":{"type":"boolean","description":"Whether the request completed successfully"},"usage":{"type":"number","description":"Total tokens consumed (input + output)"},"outputs":{"type":"array","items":{"type":"string"},"description":"Full AI-generated response"},"model":{"type":"string","description":"AI model identifier used for generation","example":"us.anthropic.claude-sonnet-4-5-20250929-v1:0"},"cached":{"type":"number","description":"Number of cached tokens used (prompt caching)"},"completion":{"type":"number","description":"Number of completion (output) tokens generated"}}},"SSEErrorEvent":{"type":"object","description":"Sent when an error occurs during processing","properties":{"type":{"type":"string","enum":["error"]},"error":{"type":"object","description":"Error details","properties":{"message":{"type":"string","description":"Human-readable error message"},"status":{"type":"number","description":"HTTP-equivalent status code"}}}}},"SSEDoneEvent":{"type":"object","description":"Terminal event indicating the stream has ended. No more events will follow.","properties":{"type":{"type":"string","enum":["done"]}}},"QandAStreamRequest":{"type":"object","description":"Request payload for SSE streaming Q&A","required":["inputs"],"properties":{"inputs":{"type":"array","items":{"type":"string"},"description":"User messages to send to the AI","example":["What is machine learning?"]},"requestArgs":{"type":"object","description":"Optional context arguments","properties":{"selectedCourse":{"$ref":"#/components/schemas/ConversationContext"},"ragOnly":{"type":"boolean","description":"Return only RAG results without AI generation","default":false},"ragIgnore":{"type":"boolean","description":"Skip RAG search entirely (LLM answers without RAG context)","default":false},"userISODate":{"type":"string","format":"date-time","description":"User's current timestamp"},"userTimezone":{"type":"string","description":"User's IANA timezone"},"userGPSCoordinates":{"$ref":"#/components/schemas/GPSCoordinates"},"institutionPublicId":{"type":"string","format":"uuid","description":"Specifies the digital twin (institution) to send the command to.\nThe server validates that the authenticated user has a valid membership\nin the specified institution. When found, the user's active institution\nis switched to this institution in their profile data for the duration\nof the request.\n"},"assistantId":{"type":"string","description":"ObjectId of the assistant to use for this request.\nTakes priority over selectedCourse.assistant._id (legacy).\nIf not provided, the assistant is resolved from selectedCourse or\nthe most recent history record for the conversation.\n"}}}}}}},"tags":[{"name":"Authentication","description":"User authentication, registration, and password management (/api/auth)"},{"name":"OAuth","description":"OAuth authentication providers - Google, GitHub, SSO (/api/auth/oauth)"},{"name":"User","description":"User profile management and account operations (/api/user)"},{"name":"User Institutions","description":"User institution memberships and switching (/api/user/institution)"},{"name":"User Tools","description":"Available tools for authenticated users (/api/user/tools)"},{"name":"Institutions","description":"Institution settings and configuration (/api/user/institution)"},{"name":"Conversation","description":"AI conversation and Q&A endpoints (/api/ai)"},{"name":"Realtime","description":"Real-time voice AI and WebRTC sessions (/api/ai/rt)"},{"name":"Assistant","description":"AI assistant configuration and management (/api/user/assistant)"},{"name":"History","description":"Conversation history and favorites (/api/user/history)"},{"name":"RAG","description":"Document upload, embedding, and retrieval-augmented generation (/api/user/files, /api/user/rag)"},{"name":"Setting","description":"Instance variables and settings management (/api/user/setting)"},{"name":"Branding","description":"Digital twin branding and customization (/api/agent/branding)"},{"name":"Agent","description":"Agent engagement and session management (/api/agent)"},{"name":"SDK Launch","description":"SDK launch token signing and verification for secure iframe embedding (/api/auth/sdk-sign, /api/auth/sdk-verify)"},{"name":"Testing","description":"Health checks, diagnostics, and test endpoints (/api/test)"},{"name":"Admin Accounts","description":"Account management for super admins (/api/admin/account)"},{"name":"Admin Institutions","description":"Institution management for admins (/api/admin/institution)"},{"name":"Admin Users","description":"User management for admins (/api/admin/user)"},{"name":"Admin Entitlements","description":"User-institution relationships and permissions (/api/admin/userInstitution)"},{"name":"Admin Sessions","description":"Session management for admins (/api/admin/session)"},{"name":"Admin Histories","description":"Conversation history management and analytics (/api/admin/history)"},{"name":"Admin Assistants","description":"AI assistant management for admins (/api/admin/assistant)"},{"name":"Admin Questions","description":"Institution question and prompt management (/api/admin/question)"},{"name":"Admin Tools","description":"Tool configuration management (/api/admin/tool)"},{"name":"Admin AI Models","description":"AI model configuration (/api/admin/aimodel)"},{"name":"Admin MCP Servers","description":"Model Context Protocol server management (/api/admin/mcpserver)"},{"name":"Admin Feedbacks","description":"User feedback management (/api/admin/feedback)"},{"name":"Admin Uploads","description":"Upload management (/api/admin/upload)"},{"name":"Admin Charts","description":"Analytics and visualization chart management (/api/admin/chart)"},{"name":"Audio Notes","description":"Capture and ingest spoken notes into the personal vault"},{"name":"Memory","description":"User-facing memory parameters (personal + shared instance memory)."},{"name":"My Data","description":"GDPR controls — personal-scope counts, async ZIP-by-email export, and scoped soft-delete. Every endpoint pins `user = req.user._id` AND `institution: null`; institution-scoped data is governed by the institution's own retention policy and never reached from here."},{"name":"Questions","description":"User-facing read of the onboarding question bank used by the \"create a digital twin\" wizard."},{"name":"Transcription","description":"One-shot speech-to-text for in-place dictation. Audio blob in, transcript out — no Upload / History / RAG embeddings are persisted. Use `/audio-notes` for anything durable."}]}