What's New
- Billing (metered, pay-for-value) — All prediction endpoints now return a
billingobject in the response. You are only charged when the algorithm returns a valid VHS measurement. Failed predictions, rejected images, and rechecks are always free. - API Key Security — API keys are now stored as SHA-256 hashes. Raw keys are never stored server-side. Treat your API key like a password — it cannot be recovered if lost.
Authentication
All endpoints require the x-api-key header. Include your API key with every request:
x-api-key: YOUR_API_KEYAPI keys are provisioned per customer. Request an API key.
Endpoints Overview
| Method | Path | Description | Billable |
|---|---|---|---|
GET | /v3/health | Service health check | No |
POST | /v3/predict | Single or batch image prediction | Yes |
POST | /v3/predict/stream | Streaming batch prediction (SSE) | Yes |
POST | /v3/pop-session/start | Start a PoP capture session | No |
POST | /v3/pop-session | Send a frame to a PoP session | Yes |
POST | /v3/corners | Detect screen corners | No |
POST | /v3/rectify | Perspective correction | No |
POST | /v3/preprocess | Corner detection + rectification | No |
GET /v3/health
Returns the current health status of the API service and loaded models.
Response
{
"status": "healthy",
"models_loaded": 5,
"pop_models_loaded": 5,
"pop_pipeline_enabled": true,
"model_version": "3.3.0-ensemble"
}Response Fields
| Field | Type | Description |
|---|---|---|
status | string | Service health status ("healthy" or "degraded") |
models_loaded | integer | Number of prediction models loaded (expected: 5) |
pop_models_loaded | integer | Number of PoP pipeline models loaded (expected: 5) |
pop_pipeline_enabled | boolean | Whether the Picture-of-Picture pipeline is available |
model_version | string | Current model version string |
POST /v3/predict
Submit one or more base64-encoded radiograph images for cardiac measurement. Returns VHS, VLAS, landmark coordinates, confidence metrics, and an annotated image.
Request Headers
| Header | Required | Value |
|---|---|---|
x-api-key | Yes | Your API key |
Content-Type | Yes | application/json |
Single Image Request
{
"image": "<base64-encoded-image>",
"uuid": "patient-123",
"render": true
}Batch Request (up to 10 images)
{
"images": ["<base64>", "<base64>"],
"uuid": "patient-123",
"render": true
}Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes | Base64-encoded JPEG or PNG image |
uuid | string | No | Patient identifier for historical tracking. Also used for 24h recheck billing grace. |
render | boolean | No | If true, returns a rendered_image with overlaid landmarks and scores. Default: false. |
images | string[] | No | Array of base64-encoded images for batch mode (max 10). Use instead of image for batch requests. |
Single Image Response
{
"vhs": 10.4,
"vlas": 2.8,
"prediction": [
{ "x": 0.42, "y": 0.15, "class": "heart_top" },
{ "x": 0.41, "y": 0.44, "class": "heart_bottom" },
{ "x": 0.25, "y": 0.30, "class": "heart_right" },
{ "x": 0.57, "y": 0.29, "class": "heart_left" },
{ "x": 0.49, "y": 0.40, "class": "VLAS" },
{ "x": 0.36, "y": 0.12, "class": "Vertebra A" },
{ "x": 0.55, "y": 0.12, "class": "Vertebra B" }
],
"result_code": 1,
"result_description": "VHS and VLAS predicted successfully with high confidence",
"model_confidence": "optimal",
"spread": 0.0032,
"pop_detected": false,
"model_version": "3.3.0-ensemble",
"rendered_image": "<base64-encoded-annotated-image>",
"historic_vhs": [["2026-03-27", 11.38]],
"historic_vlas": [["2026-03-27", 1.88]],
"billing": {
"billable": true,
"reason": "valid_vhs",
"call_type": "single",
"monthly_usage": 47,
"tier": "paygo",
"rate": 3.00
}
}Response Fields
| Field | Type | Description |
|---|---|---|
vhs | number | Vertebral Heart Score |
vlas | number | Vertebral Lung-to-Apex Scale |
prediction | object[] | Array of 7 landmark coordinates (x, y, class). Coordinates are normalized 0.0–1.0. |
result_code | integer | Numeric result code (see Result Codes) |
result_description | string | Human-readable result description |
model_confidence | string | Confidence level: "optimal", "good", or "review" |
spread | number | Ensemble spread metric (lower is better) |
pop_detected | boolean | Whether a picture-of-picture was detected and corrected |
model_version | string | Model version used for prediction |
rendered_image | string | Base64-encoded annotated image with landmarks and measurements overlaid. Only present when render: true is set in the request. |
historic_vhs | null | [date, number][] | Previous measurements for this uuid as [[date, value]] pairs. Populated when a uuid is provided. Empty array if no history exists. |
historic_vlas | null | [date, number][] | Previous measurements for this uuid as [[date, value]] pairs. Populated when a uuid is provided. Empty array if no history exists. |
billing | object | Billing metadata for this request. See Billing section. |
Rejection Response
When an image is rejected (result_code 6), the response includes additional fields: classifier_confidence, rejected, and rejection_reason. VHS and VLAS will be null.
{
"classifier_confidence": 0.7937,
"model_version": "3.3.0-ensemble",
"rejected": true,
"rejection_reason": "not_a_valid_radiograph",
"result_code": 6,
"result_description": "Image rejected — not a valid radiograph.",
"vhs": null,
"vlas": null,
"billing": {
"billable": false,
"reason": "no_valid_vhs",
"monthly_usage": 47,
"tier": "paygo"
}
}When an image is rejected, the following standard fields are absent from the response: prediction, model_confidence, spread, pop_detected, rendered_image, historic_vhs, historic_vlas.
Batch Response
{
"vhs": 11.32,
"vlas": 1.87,
"best_photo_index": 0,
"best_vhs": 11.32,
"best_vlas": 1.87,
"median_vhs": 11.32,
"median_vlas": 1.87,
"vhs_std": null,
"vlas_std": null,
"vhs_confidence": "optimal",
"vlas_confidence": "optimal",
"n_photos_processed": 2,
"n_photos_with_landmarks": 1,
"processing_started": true,
"per_photo": [
{
"vhs": 11.32,
"vlas": 1.87,
"spread": 0.0023,
"model_confidence": "optimal",
"landmarks_detected": true,
"axes_perpendicular": true,
"pop_detected": false
},
{
"vhs": null,
"vlas": null,
"spread": null,
"model_confidence": "review",
"landmarks_detected": false,
"axes_perpendicular": false,
"rejected": true,
"rejection_reason": "not_a_valid_radiograph",
"classifier_confidence": 0.7937
}
],
"historic_vhs": [["2024-01-26", 9.23]],
"historic_vlas": [["2024-01-26", 2.12]],
"model_version": "3.3.0-ensemble",
"billing": {
"billable": true,
"reason": "valid_vhs",
"call_type": "session",
"monthly_usage": 48,
"tier": "paygo",
"rate": 4.00
}
}Batch Response Fields
| Field | Type | Description |
|---|---|---|
vhs | float | null | Best VHS measurement from the batch |
vlas | float | null | Best VLAS measurement from the batch |
best_photo_index | integer | Index of the best image in the batch |
best_vhs | float | Best VHS across frames with detected landmarks |
best_vlas | float | Best VLAS across frames with detected landmarks |
median_vhs | float | Median VHS across frames with detected landmarks |
median_vlas | float | Median VLAS across frames with detected landmarks |
vhs_std | float | null | Standard deviation of VHS across frames |
vlas_std | float | null | Standard deviation of VLAS across frames |
vhs_confidence | string | Batch-level VHS confidence: optimal, good, or review |
vlas_confidence | string | Batch-level VLAS confidence: optimal, good, or review |
n_photos_processed | integer | Total number of images processed |
n_photos_with_landmarks | integer | Number of images where landmarks were detected |
processing_started | boolean | Whether processing began |
per_photo | object[] | Per-image results array |
per_photo[].vhs | float | null | VHS for this image |
per_photo[].vlas | float | null | VLAS for this image |
per_photo[].spread | float | null | Ensemble spread for this image |
per_photo[].model_confidence | string | Confidence tier for this image |
per_photo[].landmarks_detected | boolean | Whether landmarks were found |
per_photo[].axes_perpendicular | boolean | Whether cardiac axes are perpendicular |
per_photo[].pop_detected | boolean | Whether PoP was detected (only on successful frames) |
historic_vhs | array | Historical VHS as [[date, value]] pairs (top-level, not per-photo) |
historic_vlas | array | Historical VLAS as [[date, value]] pairs (top-level, not per-photo) |
billing | object | Billing metadata for this request. See Billing section. |
Error Responses
| HTTP Status | Meaning | Example Body |
|---|---|---|
400 | Bad Request — missing or invalid fields | {"error": "Missing required field: image"} |
403 | Forbidden — invalid or missing API key | {"error": "Invalid API key"} |
413 | Payload Too Large — image exceeds 10 MB | {"error": "Image exceeds maximum size of 10MB"} |
422 | Unprocessable — corner detection failed (corners/preprocess only) | {"error": "Corner detection failed"} |
500 | Internal Server Error | {"error": "Internal server error"} |
503 | Service Unavailable — PoP pipeline not loaded | {"error": "PoP pipeline not available"} |
POST /v3/predict/stream
Submit a batch of images and receive results as Server-Sent Events (SSE). The request body is the same as the batch /v3/predict endpoint.
SSE Events
processing_started
event: processing_started
data: {"n_images": 2, "model_version": "3.3.0-ensemble"}photo (emitted per image)
event: photo
data: {
"index": 0,
"vhs": 11.32,
"vlas": 1.87,
"spread": 0.0023,
"model_confidence": "optimal",
"landmarks_detected": true,
"axes_perpendicular": true,
"n_total": 2,
"model_version": "3.3.0-ensemble"
}summary
event: summary
data: {
"best_vhs": 11.32,
"best_vlas": 1.87,
"median_vhs": 11.32,
"median_vlas": 1.87,
"vhs_std": null,
"vlas_std": null,
"vhs_confidence": "optimal",
"vlas_confidence": "optimal",
"best_photo_index": 0,
"n_photos_processed": 2,
"n_photos_with_landmarks": 1
}Billing: Stream predictions are billed at the session rate ($4.00 paygo) as a single event, regardless of how many images are in the batch.
error
event: error
data: {"index": 1, "uuid": "patient-456", "error": "Invalid image data"}POST /v3/pop-session/start
Initialize a new Picture-of-Picture (PoP) capture session. The session guides the client through a multi-frame capture flow to obtain the best possible radiograph image from a screen photo.
Request
Send an empty JSON body:
POST /v3/pop-session/start
Content-Type: application/json
x-api-key: YOUR_API_KEY
{}Response
{
"session_id": "0960abaa8179476d",
"session_status": "ready",
"pop_pipeline_enabled": true,
"models_loaded": 5,
"pop_models_loaded": 5,
"model_version": "3.3.0-ensemble",
"session_config": {
"max_frames": 20,
"max_consecutive_failures": 5,
"session_ttl_seconds": 300
}
}Response Fields
| Field | Type | Description |
|---|---|---|
session_id | string | Unique session identifier to include in subsequent requests |
session_status | string | Current session state: "ready" |
pop_pipeline_enabled | boolean | Whether the PoP pipeline is available |
models_loaded | integer | Number of prediction models loaded |
pop_models_loaded | integer | Number of PoP pipeline models loaded |
model_version | string | Model version identifier |
session_config | object | Session configuration including max frames, max consecutive failures, and session TTL |
POST /v3/pop-session
Send a captured frame to an active PoP session. The server evaluates the frame quality and either requests another frame or completes the session with a prediction.
Request
{
"image": "<base64-encoded-frame>",
"session_id": "sess_abc123def456",
"uuid": "patient-123"
}Response: Continue (need more frames)
{
"session_id": "0960abaa8179476d",
"session_status": "continue",
"reason": "0/3 good frames so far, need more",
"frame_index": 0,
"frames_processed": 1,
"good_frames": 0,
"result_code": 5,
"result_description": "Neither VHS nor VLAS could be calculated — check image quality.",
"frame_result": {
"vhs": null,
"vlas": null,
"spread": null,
"model_confidence": "review",
"landmarks_detected": false,
"pop_detected": true
},
"model_version": "3.3.0-ensemble"
}No billing field is present during continue — billing is only evaluated on session completion or failure.
Response: Complete (session succeeded)
{
"session_id": "sess_abc123def456",
"session_status": "complete",
"frame_index": 3,
"rectified_image": "<base64-encoded-rectified-image>",
"prediction": {
"vhs": 10.4,
"vlas": 2.8,
"prediction": [
{ "x": 0.42, "y": 0.15, "class": "heart_top" },
{ "x": 0.41, "y": 0.44, "class": "heart_bottom" },
{ "x": 0.25, "y": 0.30, "class": "heart_right" },
{ "x": 0.57, "y": 0.29, "class": "heart_left" },
{ "x": 0.49, "y": 0.40, "class": "VLAS" },
{ "x": 0.36, "y": 0.12, "class": "Vertebra A" },
{ "x": 0.55, "y": 0.12, "class": "Vertebra B" }
],
"result_code": 0,
"result_description": "Success",
"model_confidence": "optimal",
"spread": 0.0028,
"model_version": "3.3.0-ensemble",
"rendered_image": "<base64>"
},
"billing": {
"billable": true,
"reason": "valid_vhs",
"call_type": "session",
"monthly_usage": 49,
"tier": "paygo",
"rate": 4.00
}
}Response: Failed (session could not complete)
{
"session_id": "sess_abc123def456",
"session_status": "failed",
"frame_index": 20,
"error": "max_frames_exceeded",
"billing": {
"billable": false,
"reason": "no_valid_vhs",
"monthly_usage": 49,
"tier": "paygo"
}
}A PoP session is billed as a single event at the session rate when it completes with a valid VHS. Failed sessions are never billed.
Session Completion Logic
| Condition | Result |
|---|---|
| Frame quality is sufficient | Session completes with prediction |
Frame count reaches max_frames | Session fails with max_frames_exceeded |
Session exceeds session_ttl_seconds | Session fails with session_timeout |
| Frame quality insufficient | Returns continue with frame result details |
POST /v3/corners
Detect the four corners of a radiograph screen in a photograph. Returns corner coordinates for use with the /v3/rectify endpoint.
Request
{
"image": "<base64-encoded-image>"
}Response
{
"corners": [[137.48, 100.30], [490.50, 81.32], [495.35, 421.38], [154.89, 421.57]],
"order": "TL,TR,BR,BL",
"image_width": 640,
"image_height": 482,
"model_version": "3.3.0-ensemble"
}Response Fields
| Field | Type | Description |
|---|---|---|
corners | float[][] | Four corner coordinates as [x, y] arrays in pixel space |
order | string | Corner ordering: "TL,TR,BR,BL" (top-left, top-right, bottom-right, bottom-left) |
image_width | integer | Width of the input image in pixels |
image_height | integer | Height of the input image in pixels |
model_version | string | Model version identifier |
POST /v3/rectify
Apply perspective correction to an image using the provided corner coordinates. Returns a rectified (de-warped) image.
Request
{
"image": "<base64-encoded JPEG>",
"corners": [[137.48, 100.30], [490.50, 81.32], [495.35, 421.38], [154.89, 421.57]]
}Response
{
"rectified_image": "<base64-encoded JPEG>",
"width": 346,
"height": 330,
"model_version": "3.3.0-ensemble"
}POST /v3/preprocess
Combined corner detection and perspective rectification in a single call. Equivalent to calling /v3/corners followed by /v3/rectify.
Request
{
"image": "<base64-encoded-image>"
}Response
{
"rectified_image": "<base64-encoded JPEG>",
"corners": [[137.48, 100.30], [490.50, 81.32], [495.35, 421.38], [154.89, 421.57]],
"order": "TL,TR,BR,BL",
"width": 346,
"height": 330,
"model_version": "3.3.0-ensemble"
}Landmarks
Each prediction returns 7 anatomical landmarks identified on the radiograph. Coordinates are normalized (0.0–1.0) relative to image dimensions.
| Class | Description |
|---|---|
heart_top | Cranial-most point of the cardiac silhouette |
heart_bottom | Caudal-ventral apex of the cardiac silhouette |
heart_right | Right-most (cranial) extent of the heart border |
heart_left | Left-most (caudal) extent of the heart border |
VLAS | Caudal-dorsal point where the left atrium meets the vertebral column |
Vertebra A | Cranial edge of T4 vertebra (start of vertebral measurement) |
Vertebra B | Caudal edge of the vertebra where the long-axis measurement ends |
Result Codes
| Code | Description | Billable |
|---|---|---|
0 | Abnormal result — unexpected prediction state | No |
1 | VHS and VLAS predicted successfully with high confidence | Yes |
2 | VHS and VLAS predicted successfully with moderate confidence | Yes |
3 | VHS and VLAS predicted but confidence is low — review recommended | Yes |
4 | VHS predicted successfully, VLAS could not be calculated | Yes |
5 | Neither VHS nor VLAS could be calculated — check image quality | No |
6 | Image rejected — not a valid radiograph | No |
7 | PoP image detected but perspective correction failed | No |
Measurements
VHS (Vertebral Heart Score)
The long axis (heart_top to heart_bottom) and short axis (heart_right to heart_left) are measured and mapped onto the thoracic vertebral column starting at T4 (Vertebra A).
VHS = (long_axis_vertebrae + short_axis_vertebrae)Normal range (canine): 8.5 – 10.5 vertebrae
VLAS (Vertebral Lung-to-Apex Scale)
Distance from heart_top to the VLAS landmark, measured in vertebral units.
VLAS = distance(heart_top, VLAS_landmark) in vertebral unitsNormal range (canine): < 2.5 vertebrae. Values ≥ 2.5 suggest left atrial enlargement.
Confidence Scoring
The 5-fold ensemble model produces a spread value representing the variance between individual model predictions. Lower spread indicates higher agreement and confidence.
| Confidence | Spread Threshold | Recommendation |
|---|---|---|
| Optimal | < 0.005 | Results are reliable |
| Good | < 0.008 | Results are usable; visual verification suggested |
| Review | ≥ 0.008 | Manual review recommended; consider re-submitting a higher-quality image |
Rate Limits
| Parameter | Limit |
|---|---|
| Max batch size | 10 images per request |
| Max image size | 10 MB per image |
| Accepted formats | JPEG, PNG |
| PoP session TTL | 300 seconds (5 minutes) |
| Max PoP frames per session | 20 frames |
| Request timeout | 300 seconds |
| Cold start latency | ~6–7 seconds |
| Warm inference | ~1.5 seconds per image |
Billing
Overview
The V3 API uses pay-for-value billing. You are only charged when the algorithm returns a valid VHS measurement (result codes 1–4). Failed predictions, rejected images, and utility endpoints are always free.
Billing Rules
| Rule | Description |
|---|---|
| Valid VHS = billed | Result codes 1–4 incur a charge |
| No VHS = free | Result codes 0, 5, 6, 7 are never charged |
| 24h recheck grace | Same uuid within 24 hours is free. Pass a patient identifier in the uuid field to enable this. |
| Session = one charge | Batch, stream, and PoP session calls are billed as a single event at the session rate |
| Utility endpoints = free | /health, /corners, /rectify, /preprocess, /pop-session/start are never billed |
Pricing Tiers
| Tier | Monthly Volume | Single Image | Session / Stream / PoP |
|---|---|---|---|
| Pay-as-you-go | 0–100 calls | $3.00 | $4.00 |
| Starter | 101–500 calls | $2.50 | $3.50 |
| Professional | 501–2,000 calls | $2.00 | $3.00 |
| Enterprise | 2,001+ calls | Custom | Custom |
New API keys default to the pay-as-you-go tier. Contact us for volume pricing or enterprise agreements.
Call Types
| Endpoint | Call Type | Rate |
|---|---|---|
POST /v3/predict (single image) | single | Single rate |
POST /v3/predict (batch images) | session | Session rate |
POST /v3/predict/stream | session | Session rate |
POST /v3/pop-session (on complete) | session | Session rate |
Billing Response Object
Three example shapes depending on the billing outcome:
Billable call
{
"billable": true,
"reason": "valid_vhs",
"call_type": "single",
"monthly_usage": 47,
"tier": "paygo",
"rate": 3.00
}Non-billable call
{
"billable": false,
"reason": "no_valid_vhs",
"monthly_usage": 47,
"tier": "paygo"
}Non-billable recheck
{
"billable": false,
"reason": "recheck_24h",
"monthly_usage": 47,
"tier": "paygo"
}Billing Response Fields
| Field | Type | Description |
|---|---|---|
billable | boolean | Whether this call was counted as a billable event |
reason | string | Why: "valid_vhs", "no_valid_vhs", "recheck_24h", or "billing_error" |
call_type | string | "single" or "session". Only present on billable calls. |
monthly_usage | integer | Total billable calls this billing period |
tier | string | Current pricing tier: "paygo", "starter", "professional", or "enterprise" |
rate | number | Dollar amount charged. Only present on billable calls. |
Tips for Minimizing Costs
- Always pass
uuid— enables 24h recheck grace so repeat scans of the same patient within a day are free. - Use batch or stream for multiple images of the same patient — charged once at the session rate instead of per-image.
- Pre-validate images before sending — ensure images are valid radiographs to avoid wasted requests.
Code Examples
Python — Single Image
import requests
import base64
# Read and encode the radiograph
with open("radiograph.jpg", "rb") as f:
image_data = base64.b64encode(f.read()).decode()
# Send prediction request
response = requests.post(
"https://api.radanalyzer.com/v3/predict",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"image": image_data,
"uuid": "patient-123",
"render": True
}
)
data = response.json()
print(f"VHS: {data['vhs']}")
print(f"VLAS: {data['vlas']}")
print(f"Confidence: {data['model_confidence']}")
print(f"Spread: {data['spread']}")
print(f"Result: {data['result_description']}")JavaScript — PoP Session Flow
const API_BASE = "https://api.radanalyzer.com";
const headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
};
// Step 1: Start a PoP session
const startRes = await fetch(`${API_BASE}/v3/pop-session/start`, {
method: "POST",
headers,
body: JSON.stringify({})
});
const { session_id } = await startRes.json();
// Step 2: Send frames until session completes
let sessionComplete = false;
while (!sessionComplete) {
const frameBase64 = await captureFrame(); // your capture function
const frameRes = await fetch(`${API_BASE}/v3/pop-session`, {
method: "POST",
headers,
body: JSON.stringify({
image: frameBase64,
session_id: session_id,
uuid: "patient-123"
})
});
const result = await frameRes.json();
if (result.session_status === "complete") {
console.log("VHS:", result.prediction.vhs);
console.log("VLAS:", result.prediction.vlas);
sessionComplete = true;
} else if (result.session_status === "failed") {
console.error("Session failed:", result.error);
sessionComplete = true;
} else {
console.log("Feedback:", result.feedback);
// Show feedback to user, capture another frame
}
}cURL — Health Check
curl -X GET https://api.radanalyzer.com/v3/health \
-H "x-api-key: YOUR_API_KEY"Changelog
| Version | Date | Changes |
|---|---|---|
3.3.0 | March 2026 | PoP pipeline, session-based capture, corner detection, rectification |
3.0.0 | March 2026 | Initial V3 — 5-fold ensemble, batch mode, streaming, confidence scoring |