Skip to main content

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.

Audience

Engineers integrating with the portal — CI runners, partner tooling, custom dashboards. Familiarity with HTTP, JSON, and OAuth-style bearer tokens.

Full reference

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).

Path mapping

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/register
  • POST /v1/auth/login
  • POST /v1/auth/refresh
  • POST /v1/auth/forgot-password
  • POST /v1/auth/reset-password
  • GET /v1/auth/oauth/{provider}/authorize
  • GET /v1/auth/oauth/{provider}/callback
  • POST /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 URIStatusTriggered by
…/last-super-admin409Demoting the last super-admin.
…/disk-pressure503New scan rejected because disk is above hard limit.

Pagination

List endpoints accept:

Query paramDefaultDescription
limit50Page size. Max 200.
offset00-based row offset.
sortendpoint-specificComma-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 with Retry-After: 60.
  • Forgot password (/auth/forgot-password): IP-keyed 5/minute (configurable via PASSWORD_RESET_RATE_LIMIT); per-address cooldown returned as Retry-After.
note

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