API reference
This page documents the public + user-shell HTTP surfaces exposed by the ToolNexus web application. Every endpoint here is cookie-authenticated and reachable from the same host that serves the UI — there is no separate API origin and no API-key bearer scheme today.
For the higher-level platform map (admin API surface, internal services, runtime worker proxy) see the project wiki: https://codecloudclub.github.io/ToolNexus-V2/18-Public-Api-Surface/.
Conventions
- Base URL: the same origin that serves the UI (e.g.
https://app.example.com). - Authentication: ASP.NET Core Identity session cookie. Sign in at
/auth/loginfirst; the server sets the cookie and every subsequent request must include it. - Anti-forgery: every state-changing request (
POST/PUT/DELETE) must include the__RequestVerificationTokenfield issued for the current page. Posts that omit the token are rejected with400 Bad Request. The token rotates per session — read it from the Razor@Html.AntiForgeryToken()field on the page you are calling from, or by visiting the form page first and reading the hidden input. - Content type: form submissions use
application/x-www-form-urlencoded. The vault endpoints currently do not expose a JSON variant. - Errors: validation errors surface via
ModelStateand are re-rendered onto the originating page (200 OKwith the form re-rendered, anti-forgery refreshed). Hard rejections (anti-forgery, not signed in, not found) use the standard HTTP status codes —400,401,404.
API key vault — /user/vault/*
The BYOK vault stores third-party provider credentials (OpenAI, Anthropic, etc.) on a per-user basis. Values are encrypted at rest with ASP.NET Core Data Protection using a per-user purpose string (UserVault:{userId}); the plaintext value is never logged or returned in any response. There is a fixed cap of 10 saved keys per user, and each (UserId, Provider, Name) tuple is unique — saving the same name twice updates the row by delete-and-recreate.
GET /user/vault
Renders the vault management page (Views/User/Vault/Index.cshtml). The page shows the user's saved keys (masked metadata only — provider, name, last-four), the "add a key" form, and per-row delete buttons. The response is HTML; there is no JSON variant.
Auth: session cookie required. Unauthenticated callers are redirected to /auth/login.
POST /user/vault/add
Creates a new vault entry.
| Field | Required | Type | Notes |
|---|---|---|---|
Provider |
yes | string | One of the supported provider keys (e.g. openai, anthropic). Case-insensitive. |
Name |
yes | string | A user-chosen label, unique per (UserId, Provider). Max 64 chars. |
Value |
yes | string | The plaintext provider key. Encrypted before persistence; never written to logs. |
__RequestVerificationToken |
yes | string | Anti-forgery token from the page. |
Responses
302 Foundto/user/vaulton success (the page re-renders with the new row).200 OKwith the form re-rendered if validation fails (ModelState).400 Bad Requestif the anti-forgery token is missing or invalid.401 Unauthorizedif the caller is not signed in.
Limits
- 10 keys per user, hard-capped at the service layer. The form re-renders with an error when the cap is hit.
- Duplicate
(UserId, Provider, Name)is treated as an update — the old row is deleted and the new value re-saved encrypted.
`POST /user/vault/delete/
Deletes a vault entry by row id.
| Field | Required | Type | Notes |
|---|---|---|---|
id (route) |
yes | int | Vault row id, scoped to the signed-in user. |
__RequestVerificationToken |
yes | string | Anti-forgery token from the page. |
Responses
302 Foundto/user/vaultwhether the row existed or not (delete is idempotent for the caller).400 Bad Requestif the anti-forgery token is missing or invalid.401 Unauthorizedif the caller is not signed in.404 Not Foundif the row exists but belongs to a different user (the service layer scopes deletes byuserManager.GetUserId(User)).
Encryption story
- Each user has a per-user
IDataProtectorderived from the shared Data Protection key ring, with the purpose stringUserVault:{userId}. DataProtectionUserSecretVault.Encrypt(string plaintext)callsIDataProtector.Protect; the resulting ciphertext is stored inUserVaultCredentialEntity.EncryptedValue.- Decryption happens only when the application explicitly resolves the key for an outbound call. The vault page never decrypts — it only renders the last-four characters of the cleartext that was provided at save time and stored as
MaskedValue. - Data Protection key rotation is operator-controlled; rotating the keys invalidates all encrypted vault rows. There is currently no automatic key-rotation migration path.
Runtime resolution caveat
The vault is wired for storage and management but not yet consulted at AI run time. AiRuntimeExecutor.ResolveManagedApiKey does not currently fall back to the vault — it resolves the operator's managed credentials only. Saving a personal key today persists the credential and unlocks the vault UI, but does not yet unlock private AI runs. The runtime wiring is tracked separately from the vault backend ship.
Profile settings — /user/settings/profile
The profile page exposes display-name editing, an email-change request flow with confirmation by re-clicking a link from the new address, and avatar upload. All three are cookie-authenticated and anti-forgery protected.
GET /user/settings/profile
Renders the profile settings page with the current display name, email, pending email change (if any), and avatar.
POST /user/settings/profile
Updates the display name. Trims whitespace, requires [Required], enforces StringLength(120). Refreshes the auth cookie via RefreshSignInAsync so the new display name appears in the shell without requiring a sign-out.
POST /user/settings/profile/email
Begins an email change. Validates the new address format and uniqueness, calls UserManager.GenerateChangeEmailTokenAsync, and dispatches a confirmation email to the new address (never the current address). The current email remains the authoritative login until the confirmation link is followed.
GET /user/settings/profile/confirm-email-change
Confirms the email change. Validates the user id, the new email, and the token (query-string parameters), then calls UserManager.ChangeEmailAsync. On success the email + normalized email columns are updated and a fresh confirmation cookie is issued.
POST /user/settings/profile/avatar
Uploads an avatar image. Accepts a multipart file in the Avatar field; the controller validates the MIME type (PNG or JPEG only), size (≤ 256 KiB), and persists the encrypted bytes to ApplicationUser.AvatarImage via Data Protection. The current avatar renders in _AccountMenu.cshtml and on the profile page itself.
POST /user/settings/profile/avatar/remove
Deletes the saved avatar. Clears AvatarImage and re-renders the profile page; the account menu falls back to the generated initials avatar.
Other surfaces
For the remaining public surfaces — docs search at /docs/search, the public roadmap/changelog/feedback endpoints, and the admin API — see the project wiki at https://codecloudclub.github.io/ToolNexus-V2/. The wiki is the source of truth for cross-surface reference docs; the page you are reading now is intentionally scoped to the user-shell endpoints linked from the dashboard /docs#api CTA.