VTSearch exposes a REST-style JSON API. All endpoints accept and return JSON
unless otherwise noted. File uploads use multipart/form-data.
| Section | Description |
|---|---|
| Authentication & UI | Auth status, login/logout, HuggingFace OAuth, SPA routes, static assets, health probes, version |
| Medias & Sorting | Media listing/streaming, text/learned/example sort, votes & labels, pile upload, example-media files, seed and datasource importers |
| Labeling & Diversity | The balance and its spot check, labeling status/progress, indicator history, train-and-score, coverage atlas |
| Detectors | Detector CRUD, saved-label review, detector registry, AutoFind toggle, loading |
| Datasets | Loading, importers, demos, staging, registry, media types, embedders, clippers, cleaners, converters, media-file browsing, VTSBrowse projection |
| Import & Export | Result exporters, label importers, pregen processors, autorun extractors/localizers and running them, settings importers/exporters |
| Settings | App settings, settings sources, labelset sources |
| Dashboard | Dashboard disk/RAM usage probes |
| Find, Auto-Detect & Scoring | Multi-dataset find (+ cancel/check-labels), Find Label, Auto-Detect and background AutoFind results, the test of the line, find stats/corrections/queues/evidence coverage |
| File Browser | Server filesystem browsing |
| Progress events (SSE) | GET /api/events: the single Server-Sent Events stream carrying progress for every long-running operation |
These pages explain purpose and non-obvious semantics — ordering, idempotency, async hand-offs, which errors mean what. For the exhaustive field list of any request or response, the generated spec is authoritative (see Machine-readable schema); where a page and the spec disagree on a field, trust the spec and fix the page.
Every blueprint under vtsearch/routes/ registers full paths (none sets a
url_prefix), so a route's page follows from its path:
| Path | Page |
|---|---|
/api/auth/* (including /api/auth/huggingface/*), /api/version, /healthz, /readyz, and the SPA and static paths (/, /label, /dashboard, /favicon*.ico, /logo.svg, /{path}) |
auth.md |
/api/medias/*, /api/votes*, /api/labels/*, /api/sort*, /api/learned-sort*, /api/example-sort*, /api/label-file-sort, /api/textsort-suggestions, /api/server-media-files*, /api/seed-import*, /api/datasource-import* |
medias.md |
/api/balance, /api/precision-check*, /api/labeling-progress, /api/labeling-status, /api/indicator-score-history, /api/eval/*, /api/coverage-atlas/* |
labeling.md |
/api/detectors*, except /api/detectors/{detector_name}/labelset-source* |
detectors.md |
/api/dataset/*, /api/datasets/registry*, /api/media-types, /api/embed, /api/embedders, /api/clippers, /api/cleaners, /api/converters, /api/browse-media-files*, /api/projection/* |
datasets.md |
/api/exporters*, /api/label-importers*, /api/pregen-processors*, /api/autorun-extractors*, /api/autorun-localizers*, /api/extract, /api/localize, /api/auto-extract, /api/auto-localize, /api/settings-importers*, /api/settings-exporters* |
io.md |
/api/settings, /api/settings/defaults, /api/settings-sources*, /api/labelset-sources*, /api/detectors/{detector_name}/labelset-source* |
settings.md |
/api/dashboard/* |
dashboard.md |
/api/find, /api/find/*, /api/find-label, /api/auto-detect, /api/autofind/runs/*, /api/line-test* |
find.md |
/api/browse |
file-browser.md |
/api/events |
events.md |
/api/achievements*, /api/sessions/recent, /api/jobs/active |
Other endpoints, below |
/api/openapi.json, /api/docs |
Machine-readable schema, below |
Small families with no page of their own. Field lists are in the spec. How the achievement counters, tiers and the two watermarks behind these routes work is in achievements.md.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/achievements |
Full achievement state: tier_names, achievements (per-category counter, tier_idx (-1 = locked), next_threshold), pending_announcements, pending_toasts, docs, media_types, hours. Counters are zeroed when the enable_achievements setting is off. |
| POST | /api/achievements/{category_id}/acknowledge |
Body {"tier_idx": N}. Marks that tier as announced in the panel. → {"ok", "changed"} |
| POST | /api/achievements/{category_id}/mark-toasted |
Body {"tier_idx": N}. Marks the tier's unlock toast as shown so it fires once. → {"ok", "changed"} |
| POST | /api/achievements/check-phrase |
Body {"phrase": "..."}. Checks a Readme Reader code phrase. → {"matched", "doc_id", "doc_name", "already_read"} |
| GET | /api/achievements/docs/{doc_id}/raw |
Raw markdown of a Readme Reader doc, text/plain (this file is one). 404 for an unknown id. |
| GET | /api/sessions/recent |
The current user's recently used (dataset, detector) pairs, newest first: {"sessions": [{dataset_id, detector_id, dataset_name, detector_name, last_activity}]}. Pairs whose dataset or detector was deleted are dropped on read. |
| POST | /api/sessions/recent |
Body {"dataset_id", "detector_id"}. Records activity on a pair; returns the updated list. |
| GET | /api/jobs/active |
{"busy_pairs": [{dataset_id, detector_id, job_types}]} — every pair with a running or pending background job (learned sort, eval, …). Cheap and lock-free; safe to poll every few seconds. |
Health probes and GET /api/version are in auth.md;
the VTSBrowse projection endpoints are in datasets.md.
VTSearch holds multiple loaded datasets and detectors at once, one context each. Most endpoints operate on "the active context", and the active context is chosen per request by two HTTP headers:
| Header | Selects | Sent by |
|---|---|---|
X-Dataset-Id |
Which loaded DatasetContext the request's medias / coverage / dataset-scoped votes resolve to |
Angular's HttpClient interceptor on every API call |
X-Detector-Id |
Which loaded DetectorContext the request's good_votes / bad_votes / model / labelset resolve to |
Same interceptor |
Key semantics (the before_request hook in vtsearch/hooks.py, and
vtsearch/routes/_context.py):
- Per-request, not global. The headers stash the chosen context on
flask.gfor the lifetime of the request; they do not mutate any global "currently active" state, so concurrent requests for different datasets don't interfere. - Query-param fallback. Browser-native requests that bypass the Angular
interceptor (
<img src>,<audio src>,<video src>) may pass?dataset_id=/?detector_id=instead; the header takes priority when both are present. - Required on context-mutating endpoints. Endpoints that mutate dataset or
detector state (votes, label imports, media insertion, learned sort, …) are
guarded by
require_dataset_header/require_detector_headerand return 400 with a message likeX-Dataset-Id header (or ?dataset_id= query param) is required for this endpointwhen the id can't be determined. Pure reads, registry listings, auth, and file-browser routes don't require them. - Unloaded id → 409. If a header names an id that isn't currently loaded,
the request proceeds until it touches a context proxy, then fails 409
(
DatasetNotLoadedError/DetectorNotLoadedError) rather than silently falling back to stale data. Routes that never touch the proxies still respond normally.
Per-endpoint pages note where these headers are required; when in doubt, send both for any dataset- or detector-scoped call.
| Pattern | Meaning |
|---|---|
{param} |
URL path parameter |
| Body | JSON request body (Content-Type application/json) |
| Form | multipart/form-data |
→ |
Response body |
X-Dataset-Id / X-Detector-Id |
Context headers selecting the active dataset / detector |
| Async endpoints | Return immediately (usually with a task_id or job_id); follow progress on GET /api/events (SSE) — dataset loads on the loading-tasks channel, detector loads/ingests on detector-loading-tasks |
Common error shape. Every JSON error the API returns — from a route, from
a global handler, or from schema validation — carries the same envelope
(vtsearch/errors.py, documented in the OpenAPI spec as the Error schema):
{
"code": 404,
"status": "Not Found",
"message": "Human-readable message",
"request_id": "ab12cd34ef56"
}code is the numeric HTTP status and status its name. request_id is
present on every error raised inside a request (it also comes back in the
X-Request-Id header), so a user can quote it in a bug report and an
operator can grep the structured logs for it.
Three optional fields appear when they apply, plus any endpoint-specific
extras (e.g. available, missing_fields, dataset_id):
| Field | When |
|---|---|
errors |
Schema validation failed (422); maps location → field → messages, e.g. {"json": {"volume": ["Not a valid number."]}} or {"query": {...}} |
detail |
500s from the catch-all handler; the exception type and its first line, e.g. "RuntimeError: embedder X not loaded" |
error_code |
A machine-readable slug where the client branches on the kind of failure: auth_required (401), dataset_not_loaded / detector_not_loaded (409: the id is registered but not in memory yet, so a load cures it), dataset_not_found / detector_not_found (404: the registry no longer lists the id — it was deleted), and on the routes that reach a converter output's source item, source_not_loaded (409: the source dataset is registered but not loaded; dataset_id names it) and not_derived / no_source_dataset / source_not_found (404) |
A schema-validation failure therefore looks like:
{
"code": 422,
"status": "Unprocessable Entity",
"message": "Unprocessable Entity",
"errors": {"json": {"volume": ["Not a valid number."]}},
"request_id": "45159ab44b01"
}(status is the reason phrase as the running Python spells it —
"Unprocessable Content" from 3.13 on. Match on code / errors.) Unknown
/api/* paths and wrong methods also get this envelope (JSON 404 / 405), never
the SPA's HTML.
Status codes follow standard HTTP semantics: 200 OK, 201 Created, 400 Bad
Request (handler-level validation), 401 (auth, see
auth.md), 403 (per-user access to a
registered dataset/detector), 404 Not Found, 409 Conflict, 422 (schema
validation), 500, plus 501/502 from plugin-backed routes (plugin not
implemented / upstream failure) and 503 from GET /api/events and /readyz.
A few routes add their own: 204 from the favicon and logo routes when the file
is missing, 304 from the ETag-validated audio and thumbnail routes, 410
when a registered dataset has expired, and 415 when a video needs a transcoder
the server lacks.
GET /api/openapi.json returns an OpenAPI 3.0 document describing the
running app, generated by flask-smorest from the marshmallow schemas in
vtsearch/schemas/. A browsable Swagger UI is served at GET /api/docs
(its assets load from the jsDelivr CDN, so it needs internet access). The
committed snapshot frontend/openapi.json is the same document; the Angular
client is generated from it, and ./run-tests.sh fails when the live spec
drifts from it. Use it to:
- Look up the exhaustive field list, types, enums, and defaults of any request or response.
- Browse / try endpoints live via Swagger UI.
- Generate a TypeScript / Python client.
Every route on a flask_smorest blueprint appears in the spec — path,
parameters, summary, and error responses — even when it declares no
request/response schema. A route is left without one for one of these reasons,
so for these the pages here (or the route's spec description) are the
contract:
- Plugin-field bodies. Endpoints whose body is the fields of the plugin
named in the URL —
POST /api/dataset/import/{importer_name},POST /api/dataset/stage-import/{importer_name},POST /api/label-importers/import/{importer_name},POST /api/detectors/{name}/import-labels/{importer_name},POST /api/detectors/registry/from-labelset/{importer_name},POST /api/settings-importers/import/{importer_name},POST /api/seed-import/{importer_name},POST /api/datasource-import/{importer_name}. The accepted keys are the plugin's declaredfields(fetch them from the family's list endpoint), sent as JSON, or multipart when the plugin declares afilefield. Runtime validation still goes throughvalidate_plugin_args, so a missing required field or an invalidselectvalue still returns 422 with the standarderrorsenvelope. - Binary responses. Audio/video/image/media/thumbnail/preview routes, the
dataset and portable-bundle downloads, and
GET /api/achievements/docs/{doc_id}/raw(text/plain). - Dual-mode dispatchers.
POST /api/embedaccepts eithermultipart/form-data(file upload) orapplication/json(text), decided at request time. - SPA-serving routes.
/,/label,/dashboard, favicons,/logo.svg, and the/{path}catch-all serve HTML or static files.
A few routes live on plain Flask blueprints and are not in the spec at all:
GET /api/events— the SSE stream (events.md)./api/auth/huggingface/*— the OAuth browser-redirect flow (auth.md).GET /api/openapi.jsonandGET /api/docsthemselves.GET /static/{filename}, Flask's built-in static route over the samestatic/directory the catch-all serves.
Readme Reader code phrase: json all the way down