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: the provider endpoints take and return
application/json. The profile-settings endpoints are Razor form posts (application/x-www-form-urlencoded, plusmultipart/form-datafor the avatar upload). - 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.
Bring-your-own-key providers — /user/providers/*
Per-user provider management stores third-party AI credentials (OpenAI, Anthropic, Gemini, Groq, OpenRouter, and any other OpenAI-compatible endpoint) on a per-user basis, together with the models and combos that use them. Keys are encrypted at rest with ASP.NET Core Data Protection under a per-user purpose string (AiProvider:{userId}); the plaintext is never logged and never returned — responses expose only hasKey and a masked keyHint.
Every route below is owner-scoped: the service layer filters on OwnerUserId == userManager.GetUserId(User), so a caller can never read, edit, or delete an administrator's global provider or another user's provider.
Retired predecessor. An earlier flat vault lived at
/user/vaultand stored one key per provider name. It has been removed;VaultToProviderMigrationHostedServicefolds any legacyUserVaultCredentialsrows into the provider store on startup. Requests to/user/vaultnow 404.
Limits: 10 providers per user, 20 models per provider, 10 keys ("accounts") per provider.
GET /user/providers
Renders the provider management page. HTML only; the data is fetched by the page from /user/providers/list.
Auth: session cookie required. Unauthenticated callers are redirected to /auth/login.
GET /user/providers/list
Returns the caller's providers with their models and accounts.
{
"providers": [
{
"id": "…", "name": "My OpenAI", "type": "openai-compatible",
"baseUrl": "https://api.openai.com", "status": "active",
"healthStatus": "…", "hasKey": true, "roundRobinAccounts": false,
"models": [ { "modelId": "gpt-4o-mini", "displayName": "…", "capabilities": "generation,assistant", "isEnabled": true } ],
"accounts": [ { "id": "…", "name": "Key 1", "keyHint": "sk-…abcd", "hasKey": true, "isEnabled": true, "priority": 0, "healthStatus": "…" } ]
}
]
}
POST /user/providers/create
Registers a provider. JSON body: name (required), type, baseUrl (required unless the type is a web search/fetch provider, which carries an adapter-owned base URL), apiKey (required). The server generates the id — a client-supplied id is ignored.
Responses: 200 OK with { id, name, type }; 400 Bad Request for a missing name, an invalid base URL, a missing key, or the 10-provider cap; 401 Unauthorized when not signed in.
`POST /user/providers/
Replaces the provider-level key. JSON body: apiKey (required). Returns 200 OK, or 404 Not Found when the provider is not owned by the caller.
`POST /user/providers/
Sets status to active or inactive. Any other value is 400 Bad Request.
`POST /user/providers/
Edits (name, type, baseUrl) or deletes the provider. Delete is a hard delete and cascades to the provider's models and accounts; it returns 204 No Content.
Models
| Method | Route | Purpose |
|---|---|---|
GET | /user/providers/{providerId}/models/available | Ask the provider for its model list ({ models: [...] }; a provider error returns an empty list plus error). |
POST | /user/providers/{providerId}/models | Add a model (modelId required; capabilities defaults to generation,assistant). |
POST | /user/providers/{providerId}/models/enabled?modelId=… | Enable / disable a model. |
POST | /user/providers/{providerId}/models/capabilities?modelId=… | Replace the capability list. |
DELETE | /user/providers/{providerId}/models?modelId=… | Remove a model. |
modelId travels in the query string rather than the route because provider model ids contain /, and an encoded slash in a path segment is rejected by routing.
Accounts (multiple keys per provider)
POST /user/providers/{providerId}/accounts adds a named key; …/accounts/{accountId} edits it (name, priority), …/accounts/{accountId}/apikey rotates it, …/accounts/{accountId}/enabled toggles it, and DELETE …/accounts/{accountId} removes it. With roundRobinAccounts enabled the executor rotates across the enabled accounts by ascending priority.
Connectivity checks
POST /user/providers/{providerId}/test— sends a one-word prompt through the provider's first enabled model and returns{ ok, reply, latencyMs }or{ ok: false, error }.POST /user/providers/test-batch— runs the same check across every active provider and returns per-provider results plus pass/fail totals.POST /user/providers/playground— sends an arbitrary message (providerId, optionalmodelId,message,stream). Withstream: truethe response istext/event-streamcarrying{ token }frames, then{ done, model }; providers without streaming support fall back to a single{ content }frame.
Encryption story
- Each user gets a distinct
IDataProtectorderived from the shared Data Protection key ring, with the purpose stringAiProvider:{userId}(administrator-global providers use theglobalscope). DataProtectionSecretProtector.Protect(scope, plainText)produces the ciphertext persisted in the provider / provider-account row. Even with raw database access, a ciphertext written for user A cannot be decrypted with user B's protector.- Decryption happens only when the execution path resolves a key for an outbound provider call. The management UI reads back
keyHintandhasKey, never the ciphertext or the plaintext. - Data Protection key rotation is operator-controlled. Rotating the key ring without preserving the old keys invalidates every stored credential; there is no automatic re-encryption migration.
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.