API overview
The portal exposes a REST API rooted at /v1. The full OpenAPI 3.1 schema is generated by FastAPI and served live at https://<your-portal>/api/docs (Swagger UI), /api/redoc (Redoc), and /api/openapi.json. This page is a high-level orientation.
Engineers integrating with the portal — CI runners, partner tooling, custom dashboards. Familiarity with HTTP, JSON, and OAuth-style bearer tokens.
This page is the orientation. For the complete, browsable endpoint-by-endpoint reference — request bodies, response schemas, and validation rules — see the API reference (Redoc). It is rendered from a committed OpenAPI snapshot and ships with the docs site (no running backend required).
Browser-visible paths begin with /api/.... Traefik's stripprefix middleware strips /api before forwarding to FastAPI, so the backend's internal mount points are /v1/*, /auth/*, /ws/*, /health, and FastAPI's own /docs, /redoc, /openapi.json. Operators debugging inside the backend container should drop the /api prefix.
Base URL
https://<your-portal>/v1
Trailing slashes are normalized — both /projects and /projects/ work.
Authentication
Two auth schemes are accepted on every protected endpoint. Both use the Bearer scheme — there is no separate ApiKey scheme.
Bearer JWT (interactive sessions)
Authorization: Bearer <access_token>
Issued by POST /v1/auth/login. 30-minute lifetime by default. Refresh via the rotation cookie returned at login.
API key (machine clients)
Authorization: Bearer tos_<prefix>_<secret>
The portal recognizes the tos_ prefix and routes the bearer to the API-key validator. See API keys.
Anonymous endpoints
The following do not require a JWT:
GET /health(backend liveness)GET /healthz(frontend container liveness; not a v1 surface)POST /v1/auth/registerPOST /v1/auth/loginPOST /v1/auth/refreshPOST /v1/auth/forgot-passwordPOST /v1/auth/reset-passwordGET /v1/auth/oauth/{provider}/authorizeGET /v1/auth/oauth/{provider}/callbackPOST /v1/webhooks/github(HMAC-authenticated)POST /v1/webhooks/gitlab(token-authenticated)
Errors — RFC 7807
All 4xx and 5xx responses carry Content-Type: application/problem+json with this shape:
{
"type": "https://trustedoss.io/problems/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "API key 'tos_a1b2c3d4_…' lacks required action 'scan:trigger'.",
"instance": "/v1/projects/01H…/scans"
}
Domain extensions are snake_case and modelled in the OpenAPI schema. Two well-known examples:
| Type URI | Status | Triggered by |
|---|---|---|
…/last-super-admin | 409 | Demoting the last super-admin. |
…/disk-pressure | 503 | New scan rejected because disk is above hard limit. |
Pagination
List endpoints accept:
| Query param | Default | Description |
|---|---|---|
limit | 50 | Page size. Max 200. |
offset | 0 | 0-based row offset. |
sort | endpoint-specific | Comma-separated field or -field (descending). |
Response envelope:
{
"items": [ … ],
"total": 1273,
"limit": 50,
"offset": 0
}
Surface map
The backend's internal paths (after Traefik strips /api):
POST /auth/register anonymous
POST /auth/login anonymous, bearer issue
POST /auth/refresh anonymous, rotation
POST /auth/logout
GET /auth/me self
POST /auth/forgot-password anonymous
POST /auth/reset-password anonymous
GET /auth/oauth/{provider}/authorize anonymous
GET /auth/oauth/{provider}/callback anonymous
GET /auth/me current user info (auth router)
GET /v1/users/me/notification-prefs
PUT /v1/users/me/notification-prefs
GET /v1/users/me/oauth-identities
DELETE /v1/users/me/oauth-identities/{identity_id} # gates last-OAuth + has-password
# 409 → urn:trustedoss:problem:last-oauth-link
GET /v1/projects list (team-scoped)
POST /v1/projects
GET /v1/projects/{id}
PATCH /v1/projects/{id}
DELETE /v1/projects/{id}
GET /v1/projects/{id}/sbom?format=…
GET /v1/projects/{id}/vex?format=… openvex | cyclonedx; VEX from finding triage
POST /v1/projects/{id}/vex/import consume a VEX doc (team_admin); multipart upload
GET /v1/projects/{id}/notice
GET /v1/projects/{id}/components
GET /v1/projects/{id}/scans
POST /v1/projects/{id}/scans 202 Accepted; queues a Celery task
GET /v1/projects/{id}/vulnerabilities
GET /v1/projects/{id}/licenses
GET /v1/projects/{id}/obligations
GET /v1/projects/{id}/obligations/{obligation_id}
GET /v1/projects/{id}/gate-result
GET /v1/scans list
GET /v1/scans/{id}
POST /v1/scans/{id}/post-pr-comment
GET /v1/components/{component_id}
GET /v1/license_findings/{finding_id}
GET /v1/vulnerability_findings/{finding_id}
PATCH /v1/vulnerability_findings/{finding_id}/status # VEX state, If-Match required
GET /v1/approvals
GET /v1/approvals/{id}
POST /v1/approvals
PATCH /v1/approvals/{id}/transition # If-Match required
DELETE /v1/approvals/{id}
GET /v1/notifications
GET /v1/notifications/unread-count
PATCH /v1/notifications/read-all
PATCH /v1/notifications/{id}/read
GET /v1/api-keys
POST /v1/api-keys
DELETE /v1/api-keys/{id} revoke
POST /v1/webhooks/github anonymous, HMAC
POST /v1/webhooks/gitlab anonymous, token
# /v1/admin/** — super_admin only (404-existence-hide for non-admins)
GET /v1/admin/users
GET /v1/admin/users/{id}
PATCH /v1/admin/users/{id}/role
PATCH /v1/admin/users/{id}/deactivate
PATCH /v1/admin/users/{id}/activate
POST /v1/admin/users/{id}/password-reset
GET /v1/admin/teams
POST /v1/admin/teams
GET /v1/admin/teams/{id}
PATCH /v1/admin/teams/{id}
DELETE /v1/admin/teams/{id}
POST /v1/admin/teams/{id}/members
DELETE /v1/admin/teams/{id}/members/{user_id}
GET /v1/admin/scans global queue
POST /v1/admin/scans/{scan_id}/cancel cancel a running scan
GET /v1/admin/audit query the audit log
GET /v1/admin/audit/export.csv streaming CSV
GET /v1/admin/health component liveness
GET /v1/admin/disk
GET /v1/admin/backup list backups
POST /v1/admin/backup trigger a manual backup
GET /v1/admin/backup/{name}/download
POST /v1/admin/backup/restore upload + restore (typing-gated)
DELETE /v1/admin/backup/{name}
The full schema (request bodies, response shapes, validation rules) lives at /api/docs on every running install.
Optimistic concurrency
Endpoints that mutate domain rows with stateful workflows accept (and require) the If-Match request header carrying the row's current version integer. PATCH /v1/approvals/{id}/transition and PATCH /v1/vulnerability_findings/{finding_id}/status both use this pattern. Mismatches return 412 Precondition Failed with a Problem Details body that includes the current version.
WebSockets
The portal exposes one WebSocket endpoint:
WSS /api/ws/scans/{scan_id}
(After Traefik strips /api, the backend handles this at /ws/scans/{scan_id}.)
Authentication is handled by the first message the client sends, not by query string or headers:
{ "type": "auth", "token": "<JWT access token>" }
The gateway closes the connection with code 1008 / reason auth_timeout if the first frame does not arrive within WEBSOCKET_AUTH_TIMEOUT_SECONDS (default 1.0 s). Subsequent server frames carry progress events:
{ "percent": 70, "step": "dt_upload", "ts": "2026-05-10T12:34:56Z" }
Reconnect with exponential backoff. Each reconnect receives one initial-sync frame from the current scan row before live events flow.
Per-user concurrent connections are capped by WEBSOCKET_MAX_CONNECTIONS_PER_USER (default 3); the 4th connection evicts the oldest with code 1001 (reason="newer_connection").
OpenAPI download
curl -sS https://trustedoss.example.com/api/openapi.json > openapi.json
The schema is regenerated at startup. Pin against a release tag if you generate clients (openapi-generator-cli, openapi-typescript).
Rate limits
- Login (
/auth/login): IP-keyed 5/minute. 429 withRetry-After: 60. - Forgot password (
/auth/forgot-password): IP-keyed 5/minute (configurable viaPASSWORD_RESET_RATE_LIMIT); per-address cooldown returned asRetry-After.
Idempotency-Key request handling and X-RateLimit-* response headers are on the roadmap and are not implemented in this release.
Cancelling scans
Regular users do not cancel scans directly. Operators cancel via POST /v1/admin/scans/{scan_id}/cancel (super-admin only).
Observability
Set X-Request-ID on outbound calls; the portal echoes it in the response and logs it on every line for that request. Without the header, the portal generates a UUIDv7 and returns it.
Versioning
The path includes /v1. Breaking changes go to /v2. Within /v1:
- New optional fields on responses are not breaking.
- New required fields on requests are gated behind a new endpoint or behind a feature header.
See also
- API reference (Redoc) — full endpoint-by-endpoint schema, hosted with the docs.
/api/docs(Swagger UI) on every install.- Architecture
- API keys
- Webhooks