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/jsonon 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
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /usg/v1/health | No | Liveness probe. |
GET | /usg/v1/version | No | Active model version, feature list, and thresholds. |
POST | /usg/v1/predict | Yes | Run a USG impairment prediction. |
OPTIONS | /usg/v1/predict | No | CORS 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_KEYFailure modes
| HTTP | error.code | Meaning |
|---|---|---|
| 401 | missing_api_key | x-api-key header was not sent. |
| 403 | invalid_api_key | Header 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_KEYRequest 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.
| Field | Units | Plausible range | Notes |
|---|---|---|---|
BUN | mg/dL | 1 – 300 | Blood urea nitrogen. |
CREATININE | mg/dL | 0.1 – 30 | If submitted value is > 50, service auto-converts from µmol/L (÷ 88.4) and adds a warnings[] entry. |
HGB | g/dL | 1 – 25 | Hemoglobin. |
ABSOLUTE LYMPHOCYTES | /µL | 50 – 50 000 | If submitted value is < 50, service auto-scales by ×1000 (k/µL → /µL) and adds a warnings[] entry. |
age_months | months | 1 – 360 | Patient age. For ages in years, multiply by 12 before sending. |
JSON key note.
ABSOLUTE LYMPHOCYTESis 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.
| Field | Type | Purpose |
|---|---|---|
clinic_code | string | Stable clinic identifier. Powers per-clinic usage and outcome reports. |
uuid | string | Stable 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
| Path | Type | Meaning |
|---|---|---|
result.classification | string | "adequate" if probability_impaired < threshold_impaired, else "impaired". |
result.probability_adequate | number | 1 - probability_impaired, rounded to 4 dp. |
result.probability_impaired | number | Calibrated probability of impaired USG, rounded to 4 dp. |
result.risk_tier | string | One of low, moderate_low, moderate_high, high. See risk-tier table. |
result.risk_label | string | Human-readable phrasing of the tier, suitable for clinical notes. |
result.threshold_sg | number | USG cutoff (1.035) the model was trained against. Echoed for clarity on PDF reports. |
result.model_version | string | Model version that produced this specific prediction. |
result.explanation.base_value | number | TreeSHAP base value (log-odds). Sum of base_value + all features[].shap reproduces model log-odds output. |
result.explanation.features[] | array | Per-feature TreeSHAP attribution, sorted by abs(shap) descending. |
result.explanation.features[].feature | string | Feature key as accepted in the request body. |
result.explanation.features[].label | string | Display label suitable for UI / PDF. |
result.explanation.features[].value | number | Numeric value used by the model — post auto-correction, rounded to 2 dp. |
result.explanation.features[].shap | number | Signed TreeSHAP contribution (log-odds), rounded to 4 dp. |
result.explanation.features[].direction | string | "impaired" if shap > 0, else "adequate". |
result.warnings | array | Present only when auto-correction fired. Strings explaining what was changed and why. |
meta.service | string | "usg-v1" — useful for log routing on the client side. |
meta.model_version | string | Same as result.model_version. Provided in meta so it can be persisted without parsing the result body. |
meta.elapsed_ms | number | Server-side wall time between request entry and response. |
Risk tiers
| Tier | probability_impaired | Recommended action |
|---|---|---|
low | < 0.20 | No follow-up indicated. |
moderate_low | 0.20 – 0.3496 | Likely adequate; consider confirming with UA. |
moderate_high | 0.3496 – 0.75 | Possible impairment; UA recommended. |
high | >= 0.75 | High 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.
| Trigger | Action | Warning string format |
|---|---|---|
CREATININE > 50 | divide by 88.4 (µmol/L → mg/dL) | "Creatinine (mg/dL): 132.6 → 1.50 (converted from umol/L to mg/dL)" |
0 < ABSOLUTE LYMPHOCYTES < 50 | multiply 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." } }| HTTP | code | When |
|---|---|---|
| 400 | invalid_json | Request body could not be parsed as JSON. |
| 400 | invalid_payload | Body parsed, but is not a JSON object (e.g. an array or string). |
| 400 | missing_features | One or more of the five required features was absent. |
| 400 | out_of_range | A numeric feature was outside its plausible range after correction. |
| 400 | invalid_value | A numeric feature could not be coerced to a float. |
| 401 | missing_api_key | x-api-key header was not sent. |
| 403 | invalid_api_key | Key is not active. |
| 500 | internal_error | Unhandled 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.
uuidand recheck dedupe. Sending the sameuuidtwice within 24 hours short-circuits the billing counter. The prediction itself runs every time — the server does not return a cached result.