← Back to Developersusg/v1 · model 1.5a

USG Screening API Reference

Feline urine specific gravity screening from routine bloodwork. Served at api.radanalyzer.com/usg/v1.

Same host, same credential. The USG route lives on the same api.radanalyzer.com host as the cardiac VHS / VLAS API and accepts the same x-api-key credential. Existing customers do not need a new key — usage on /usg/v1/* is metered against the same combined volume tier.

Overview

The USG (Urine Specific Gravity) classifier predicts whether a feline patient's urine is likely impaired (USG < 1.035) from five routine bloodwork values. Intended use is to flag patients whose chemistry suggests reduced urine concentrating ability so a follow-up urinalysis can be ordered — the test that actually reveals early kidney dysfunction.

  • Base URL: https://api.radanalyzer.com
  • Path prefix: /usg/v1
  • Latency: ~200 ms warm, ~20 s cold
  • Transport: HTTPS only. Plaintext HTTP is redirected.
  • Content-Type: application/json on every POST.

See the product page for clinical background, the data hub for current performance metrics, and the standalone web tool to try the model interactively before integrating.

Endpoints

MethodPathAuthPurpose
GET/usg/v1/healthNoLiveness probe.
GET/usg/v1/versionNoActive model version, feature list, and thresholds.
POST/usg/v1/predictYesRun a USG impairment prediction.
OPTIONS/usg/v1/predictNoCORS preflight. Returns 204 No Content.

Every response includes permissive CORS headers (Access-Control-Allow-Origin: *, methods GET, POST, OPTIONS, headers Content-Type, x-api-key). Production callers should still proxy through their own backend — see operational notes.

Authentication

Send your API key in the x-api-key header. Required on POST /usg/v1/predict; not required on /health or /version. The same key authorizes /vhs/v3/* and /usg/v1/* — there is no per-algorithm key. Bearer tokens, OAuth, query-string keys, and mTLS are not supported.

x-api-key: YOUR_API_KEY

Failure modes

HTTPerror.codeMeaning
401missing_api_keyx-api-key header was not sent.
403invalid_api_keyHeader sent but key is not active.

Treat 401 and 403 the same way: prompt the operator for a new key. Keys are not recoverable through the API — request a rotation.

GET /usg/v1/health

Liveness probe. Unauthenticated. Used by clients to confirm reachability.

Response — 200 OK

{
  "status": "ok",
  "service": "usg-v1",
  "model_version": "1.5a",
  "service_version": "1.0.2"
}

GET /usg/v1/version

Returns the active model version, feature list, and decision thresholds. Useful for client-side schema validation and reproducibility logging. Record model_version alongside every stored prediction.

Response — 200 OK

{
  "service": "usg-v1",
  "service_version": "1.0.2",
  "model_version": "1.5a",
  "features": ["BUN", "CREATININE", "HGB", "ABSOLUTE LYMPHOCYTES", "age_months"],
  "threshold_impaired": 0.3496,
  "sg_cutoff": 1.035
}

POST /usg/v1/predict

Run a USG impairment prediction.

Request headers

POST /usg/v1/predict HTTP/1.1
Host: api.radanalyzer.com
Content-Type: application/json
x-api-key: YOUR_API_KEY

Request body

{
  "BUN": 22,
  "CREATININE": 1.3,
  "HGB": 14.2,
  "ABSOLUTE LYMPHOCYTES": 2400,
  "age_months": 96,

  "clinic_code": "rc-vet",
  "uuid": "pat-abc-123"
}

A Firebase callable-style envelope is also accepted for compatibility — if body.data is a JSON object, it is unwrapped and used as the payload:

{ "data": { "BUN": 22, "CREATININE": 1.3, "HGB": 14.2, "ABSOLUTE LYMPHOCYTES": 2400, "age_months": 96 } }

Required features

All five must be present and numeric. Missing any → 400 missing_features.

FieldUnitsPlausible rangeNotes
BUNmg/dL1 – 300Blood urea nitrogen.
CREATININEmg/dL0.1 – 30If submitted value is > 50, service auto-converts from µmol/L (÷ 88.4) and adds a warnings[] entry.
HGBg/dL1 – 25Hemoglobin.
ABSOLUTE LYMPHOCYTES/µL50 – 50 000If submitted value is < 50, service auto-scales by ×1000 (k/µL → /µL) and adds a warnings[] entry.
age_monthsmonths1 – 360Patient age. For ages in years, multiply by 12 before sending.

JSON key note. ABSOLUTE LYMPHOCYTES is the literal key — uppercase, with a space. It mirrors the original lab-export column name and the model's internal feature names, and is preserved in v1 for parity with the legacy integration. JavaScript callers must use bracket access: body["ABSOLUTE LYMPHOCYTES"]. A snake_case alias may be added in a future v1 minor release; v2 will rename outright.

Optional fields

Stored on the prediction log; do not affect the model. Silently ignored if absent.

FieldTypePurpose
clinic_codestringStable clinic identifier. Powers per-clinic usage and outcome reports.
uuidstringStable patient identifier. Required to participate in the 24-hour recheck-grace billing window.

Response — 200 OK

{
  "result": {
    "classification": "adequate",
    "probability_adequate": 0.8421,
    "probability_impaired": 0.1579,
    "risk_tier": "low",
    "risk_label": "Low risk of impairment",
    "threshold_sg": 1.035,
    "model_version": "1.5a",
    "explanation": {
      "base_value": -0.42,
      "features": [
        {"feature": "CREATININE",           "label": "Creatinine",       "value": 1.30,   "shap": -0.31, "direction": "adequate"},
        {"feature": "BUN",                  "label": "BUN",              "value": 22.00,  "shap": -0.18, "direction": "adequate"},
        {"feature": "HGB",                  "label": "Hemoglobin",       "value": 14.20,  "shap":  0.04, "direction": "impaired"},
        {"feature": "ABSOLUTE LYMPHOCYTES", "label": "Abs. Lymphocytes", "value": 2400.0, "shap": -0.02, "direction": "adequate"},
        {"feature": "age_months",           "label": "Age (months)",     "value": 96.0,   "shap":  0.01, "direction": "impaired"}
      ]
    }
  },
  "meta": {
    "service": "usg-v1",
    "model_version": "1.5a",
    "elapsed_ms": 187.2
  }
}

Response field reference

PathTypeMeaning
result.classificationstring"adequate" if probability_impaired < threshold_impaired, else "impaired".
result.probability_adequatenumber1 - probability_impaired, rounded to 4 dp.
result.probability_impairednumberCalibrated probability of impaired USG, rounded to 4 dp.
result.risk_tierstringOne of low, moderate_low, moderate_high, high. See risk-tier table.
result.risk_labelstringHuman-readable phrasing of the tier, suitable for clinical notes.
result.threshold_sgnumberUSG cutoff (1.035) the model was trained against. Echoed for clarity on PDF reports.
result.model_versionstringModel version that produced this specific prediction.
result.explanation.base_valuenumberTreeSHAP base value (log-odds). Sum of base_value + all features[].shap reproduces model log-odds output.
result.explanation.features[]arrayPer-feature TreeSHAP attribution, sorted by abs(shap) descending.
result.explanation.features[].featurestringFeature key as accepted in the request body.
result.explanation.features[].labelstringDisplay label suitable for UI / PDF.
result.explanation.features[].valuenumberNumeric value used by the model — post auto-correction, rounded to 2 dp.
result.explanation.features[].shapnumberSigned TreeSHAP contribution (log-odds), rounded to 4 dp.
result.explanation.features[].directionstring"impaired" if shap > 0, else "adequate".
result.warningsarrayPresent only when auto-correction fired. Strings explaining what was changed and why.
meta.servicestring"usg-v1" — useful for log routing on the client side.
meta.model_versionstringSame as result.model_version. Provided in meta so it can be persisted without parsing the result body.
meta.elapsed_msnumberServer-side wall time between request entry and response.

Risk tiers

Tierprobability_impairedRecommended action
low< 0.20No follow-up indicated.
moderate_low0.20 – 0.3496Likely adequate; consider confirming with UA.
moderate_high0.3496 – 0.75Possible impairment; UA recommended.
high>= 0.75High risk; UA strongly recommended.

The boundary 0.3496 is the same threshold_impaired returned by /version — the binary classification flips at that point, while risk_tier provides a finer-grained band suitable for triage UIs.

Auto-correction warnings

Two unit-mismatch corrections are applied automatically before scoring. When either fires, the corrected value is used for the prediction and echoed in result.explanation.features[].value, and a string is appended to result.warnings[]. Out-of-range values that fall outside the plausible band even after correction return 400 out_of_range.

TriggerActionWarning string format
CREATININE > 50divide by 88.4 (µmol/L → mg/dL)"Creatinine (mg/dL): 132.6 → 1.50 (converted from umol/L to mg/dL)"
0 < ABSOLUTE LYMPHOCYTES < 50multiply by 1000 (k/µL → /µL)"Abs. Lymphocytes (/uL): 2.4 → 2400 (value appears to be in thousands — converted to /uL)"

Errors

All errors share the umbrella envelope:

{ "error": { "code": "snake_case_code", "message": "Human-readable detail." } }
HTTPcodeWhen
400invalid_jsonRequest body could not be parsed as JSON.
400invalid_payloadBody parsed, but is not a JSON object (e.g. an array or string).
400missing_featuresOne or more of the five required features was absent.
400out_of_rangeA numeric feature was outside its plausible range after correction.
400invalid_valueA numeric feature could not be coerced to a float.
401missing_api_keyx-api-key header was not sent.
403invalid_api_keyKey is not active.
500internal_errorUnhandled exception. Logged for follow-up; safe to retry once.

code values are stable — safe to branch on programmatically. message strings may evolve and must not be parsed.

End-to-end examples

cURL

KEY=ra_live_xxxxxxxxxxxxxxxxxxxxxxxx

# Reachability
curl https://api.radanalyzer.com/usg/v1/health
curl https://api.radanalyzer.com/usg/v1/version

# Prediction
curl -X POST https://api.radanalyzer.com/usg/v1/predict \
  -H "x-api-key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "BUN": 22,
    "CREATININE": 1.3,
    "HGB": 14.2,
    "ABSOLUTE LYMPHOCYTES": 2400,
    "age_months": 96,
    "clinic_code": "rc-vet",
    "uuid": "pat-buddy-001"
  }'

Python (requests)

import os
import requests

BASE_URL = "https://api.radanalyzer.com"
API_KEY  = os.environ["RA_KEY"]   # same key authorizes /vhs/v3 and /usg/v1

resp = requests.post(
    f"{BASE_URL}/usg/v1/predict",
    headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
    json={
        "BUN": 22,
        "CREATININE": 1.3,
        "HGB": 14.2,
        "ABSOLUTE LYMPHOCYTES": 2400,
        "age_months": 96,
        "clinic_code": "rc-vet",
        "uuid": "pat-buddy-001",
    },
    timeout=15,
)
resp.raise_for_status()
data = resp.json()

result = data["result"]
print(f"{result['classification']:>9}  "
      f"p_impaired={result['probability_impaired']:.4f}  "
      f"tier={result['risk_tier']}  "
      f"model={data['meta']['model_version']}")

Node (fetch, ≥18)

const BASE_URL = "https://api.radanalyzer.com";
const API_KEY  = process.env.RA_KEY;

const resp = await fetch(`${BASE_URL}/usg/v1/predict`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
  body: JSON.stringify({
    BUN: 22,
    CREATININE: 1.3,
    HGB: 14.2,
    "ABSOLUTE LYMPHOCYTES": 2400,
    age_months: 96,
    clinic_code: "rc-vet",
    uuid: "pat-buddy-001",
  }),
});

if (!resp.ok) {
  const { error } = await resp.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const { result, meta } = await resp.json();
console.log(`${result.classification} | p=${result.probability_impaired} | ${result.risk_tier} | model=${meta.model_version}`);

Operational notes

  • Cold starts. First request after the service has scaled to zero takes ~20 s; subsequent calls in the warm window are ~200 ms. A single ~20 s response should not be interpreted as a regression.
  • Browser embedding. Do not embed ra_live_* keys in shipped JavaScript. Proxy through a backend. The CORS allow-list is permissive only because legitimate callers may run from desktop apps and clinic SaaS frontends — it is not an invitation to ship the key client-side.
  • Idempotency. All POSTs are idempotent on the wire. Safe to retry on 408 / 500 / 503.
  • uuid and recheck dedupe. Sending the same uuid twice within 24 hours short-circuits the billing counter. The prediction itself runs every time — the server does not return a cached result.

Ready to integrate?

Request an API key and start calling /usg/v1/predict today. Your existing VHS / VLAS key already works on this route.