This document describes the REST API for the XRayVision application, a DICOM processor with OpenAI integration.
The API is accessible at http://localhost:8000 by default. The port can be configured in the xrayvision.cfg file under [dashboard] section with the DASHBOARD_PORT parameter.
All API endpoints (except static file serving) require HTTP Basic Authentication. Credentials are configured in the xrayvision.cfg file under the [users] section.
Authorization: Basic base64(username:password)
Two roles exist: admin (full access, unredacted names) and user (radiologist names anonymized to initials).
Heavy AI endpoints (/api/check, /api/analyse, /api/translate) are limited to 10 requests per IP per minute. All other /api/* endpoints are limited to 60 requests per IP per minute. Exceeded limits return HTTP 429.
{
"uid": "string",
"patient": {
"name": "string",
"cnp": "string",
"id": "string",
"age": "integer",
"birthdate": "string (YYYY-MM-DD)",
"sex": "string (M|F|O)",
"county": "integer"
},
"exam": {
"created": "string (datetime)",
"date": "string (YYYYMMDD)",
"time": "string (HHMMSS)",
"protocol": "string",
"region": "string",
"status": "string (none|queued|processing|done|error|ignore|requeue)",
"type": "string",
"study": "string",
"series": "string",
"id": "string"
},
"report": {
"ai": {
"text": "string",
"summary": "string",
"created": "string (datetime)",
"updated": "string (datetime)",
"positive": "integer (-1|0|1)",
"severity": "integer (-1..10)",
"confidence": "integer (-1..100)",
"model": "string",
"latency": "integer"
},
"rad": {
"text": "string",
"text_en": "string or null",
"positive": "integer (-1|0|1)",
"severity": "integer (-1..10)",
"summary": "string",
"created": "string (datetime)",
"updated": "string (datetime)",
"id": "string",
"type": "string",
"radiologist": "string (initials for non-admin)",
"justification": "string",
"model": "string",
"latency": "integer"
},
"correct": "integer (-1|0|1)",
"reviewed": "integer (0|1)"
}
}Serve the main dashboard HTML page.
Serve the statistics HTML page.
Serve the radiologist statistics HTML page.
Serve the diagnostics statistics HTML page.
Serve the insights HTML page.
Serve the about HTML page.
Serve the report check HTML page.
Serve the favicon.ico file.
Establishes a persistent WebSocket connection for real-time dashboard updates. The server pushes a JSON frame to all connected clients whenever state changes (new exam, processing complete, review submitted, etc.).
Authentication: same session cookie as REST endpoints.
Every frame is a JSON object. All fields are always present; event is only included when a named event triggered the broadcast.
{
"dashboard": {
"queue_size": 2,
"check_queue_size": 0,
"processing": "J.D.",
"error_count": 1,
"ignore_count": 4,
"success_count": 37
},
"openai": {
"url": "http://192.168.3.238:1234/v1/chat/completions",
"health": {
"pri": true,
"sec": false
}
},
"timings": {
"avg_latency": 12.4,
"min_latency": 8.1,
"max_latency": 31.7
},
"next_query": "2026-06-07 23:45:00",
"event": {
"name": "new_exam",
"payload": { "uid": "1.2.3...", "positive": true, "reviewed": false, "severity": 7 }
}
}next_query is "Disabled" when NO_QUERY=True.
event.name |
Triggered by | event.payload |
|---|---|---|
connected |
Client just connected | { "address": "127.0.0.1" } |
processing_start |
Exam dequeued and sent to AI | { "uid", "patient": "J.D.", "region": "chest" } |
new_exam |
AI analysis complete | { "uid", "positive", "reviewed", "severity" } |
error |
Exam processing failed | { "uid", "reason": "max_retries" | "<exception>" } |
radreview |
Radiologist submitted a review | full exam object (same shape as /api/exams/{uid}) |
radreport |
Radiologist report fetched from FHIR | { "uid", "rad_report": { ... } } |
radcheck |
AI cross-check of a rad report complete | { "uid", "positive", "severity", "summary", "confidence" } |
requeue |
Exam manually re-queued | { "uid", "status": "requeue" } |
Frames without an event key are heartbeat/state-sync broadcasts (e.g. after queue size changes or FHIR loop completes).
const ws = new WebSocket(`ws://${location.host}/ws`);
ws.onmessage = (msg) => {
let data;
try { data = JSON.parse(msg.data); } catch { return; }
// Always update dashboard state
updateQueueDisplay(data.dashboard);
updateAIHealth(data.openai);
// Handle named events
if (data.event) {
switch (data.event.name) {
case 'new_exam': handleNewExam(data.event.payload); break;
case 'radreview': handleReview(data.event.payload); break;
case 'requeue': handleRequeue(data.event.payload); break;
}
}
};Important: always wrap JSON.parse in try/catch — a malformed frame must not kill the handler.
Provide paginated exam data with optional filters.
Query Parameters:
page(integer, default: 1) — Page numberreviewed(string: any|yes|no) — Filter by review status (yes = severity > -1)positive(string: any|yes|no) — Filter by AI predictioncorrect(string: any|yes|no) — Filter by AI correctnessregion(string) — Filter by anatomic region (partial match)status(string) — Filter by processing statussearch(string) — Filter by patient name, CNP, patient ID, or UIDdiagnostic(string) — Filter by radiologist diagnostic summaryradiologist(string) — Filter by radiologist nameseverity(string) — Filter by severity with interval notation (e.g.3-6,-8,2-,5)confidence(string) — Filter by AI confidence with interval notation (e.g.80-,-49,50-79)
Response:
{
"exams": ["<Exam object>"],
"total": "integer",
"pages": "integer",
"filters": "object"
}Provide a single exam's data by UID.
Path Parameters:
uid(string) — Exam UID
Response: Exam object
Error (404):
{"error": "Exam not found"}Provide paginated patient data with optional filters.
Query Parameters:
page(integer, default: 1) — Page numbersearch(string) — Filter by patient name or CNP
Response:
{
"patients": [
{
"cnp": "string",
"id": "string",
"name": "string",
"age": "integer",
"birthdate": "string (YYYY-MM-DD)",
"sex": "string (M|F|O)"
}
],
"total": "integer",
"pages": "integer",
"filters": "object"
}Provide a single patient's data by CNP.
Path Parameters:
cnp(string) — Patient CNP
Response:
{
"cnp": "string",
"id": "string",
"name": "string",
"age": "integer",
"birthdate": "string (YYYY-MM-DD)",
"sex": "string (M|F|O)",
"exams": ["string (uid)"]
}Error (404):
{"error": "Patient not found"}Provide overall statistical data for the dashboard.
Response:
{
"total": "integer",
"reviewed": "integer",
"positive": "integer",
"correct": "integer",
"wrong": "integer",
"region": "object (per-region breakdown)",
"trends": "object",
"monthly_trends": "object",
"avg_processing_time": "number",
"throughput": "number",
"error_stats": "object"
}Provide per-radiologist performance statistics. Non-admin users receive anonymized names (initials only).
Response: Object keyed by radiologist name/initials, each value containing report counts, accuracy, and timing metrics.
Provide monthly report counts per radiologist over the last 12 months. Non-admin users receive anonymized names.
Response: Object keyed by month (YYYY-MM), each value an object of radiologist → count.
Provide per-diagnostic performance statistics including accuracy, sensitivity, and PPV.
Response: Object keyed by diagnostic summary, each value containing counts and statistical metrics.
Provide analytical insights including age distribution, AI processing times, and radiologist workload metrics.
Response:
{
"age_distribution": "object",
"processing_times": "object",
"radiologist_metrics": "object"
}Provide the list of supported anatomic regions.
Response:
["chest", "abdomen", "..."]Provide distinct diagnostic summaries and their report counts.
Response: Object mapping diagnostic summary string to report count integer.
Provide monthly counts for the top 10 diagnostics over the last 12 months.
Response: Object keyed by month (YYYY-MM), each value an object of diagnostic → count.
Provide distinct radiologist names. Non-admin users receive initials only.
Response:
["string"]Provide severity levels and their report counts.
Response: Object mapping severity level (string key "0"–"10") to report count integer.
Provide global configuration parameters to the frontend.
Response:
{
"OPENAI_URL_PRIMARY": "string",
"OPENAI_URL_SECONDARY": "string",
"NTFY_URL": "string",
"AE_TITLE": "string",
"AE_PORT": "integer",
"REMOTE_AE_TITLE": "string",
"REMOTE_AE_IP": "string",
"REMOTE_AE_PORT": "integer"
}Record a radiologist's review of an exam as normal or abnormal.
Request Body:
{
"uid": "string",
"normal": "boolean"
}Response:
{"status": "success"}Re-queue an exam for AI processing (sets status to requeue).
Request Body:
{"uid": "string"}Response:
{"status": "string", "message": "string"}Trigger a manual DICOM C-FIND/C-MOVE query for recent studies.
Request Body:
{"hours": "integer (default: 3)"}Response:
{"status": "string", "message": "string"}Retrieve radiologist report for an exam from FHIR and queue it for LLM processing.
Request Body:
{"uid": "string"}Response:
{"status": "string", "message": "string"}Error (400): Missing or invalid UID.
These endpoints are rate-limited to 10 requests per IP per minute and return HTTP 429 when exceeded.
Analyze a free-text radiology report for pathological findings.
Request Body:
{"report": "string"}Response:
{
"pathologic": "string (yes|no)",
"severity": "integer (0-10)",
"summary": "string (1-3 words)"
}Error (400): Empty report text.
Perform detailed three-pass analysis of a radiology report.
Request Body:
{"report": "string"}Response:
{
"first_pass": {"topic": "string", "purpose": "string", "primary_findings": "string"},
"second_pass": {"main_points": ["string"], "supporting_evidence": ["string"], "conclusions_valid": "boolean"},
"third_pass": {"assumptions": ["string"], "missing_info": ["string"], "critique": "string"},
"overall_assessment": "string"
}Error (400): Empty report text.
Translate a Romanian radiology report to English.
Request Body:
{"report": "string"}Response:
{"translation": "string"}Serve the OpenAPI specification file (static/spec.json).
Serve image files (PNG, DCM) from the images/ directory.
Serve static files (CSS, JS, HTML, WAV, PNG, ICO) from the static/ directory.