Composite Disability Risk Index
Status:
stableCategory:compositeAdded:2026-04-20Last reviewed:2026-06-03
Summary
Trinity's flagship composite analytics endpoint. One call accepts any subset of the 14 scoreable instruments plus optional demographics and returns a single cross-domain risk readout:
- A
cdri_score(0–100) — the headline overall disability-related risk indicator. - A
magnitudesblock: seven phenotype-named severities (0.0–1.0), one per recovery phenotype, plus anoverall_magnitude. - A
phenotypeblock attaching one of seven recovery phenotypes, with a distribution across all seven and a certainty. - A
findingsarray carrying every instrument that actually scored. - A
red_flagsarray surfacing any safety escalations detected. - A
confidencereport the caller can use to decide how much weight to put on the result. - Optionally, an
acuityblock (priority-of-risk-against-time) when the caller also supplies de-identifiedsituationandonsetinputs.
Unlike /score, which rejects any request
missing a required item, /cdri/compute is permissive on input:
any subset is valid, including a body carrying only demographics (or
no body at all). Missing signal lowers confidence; it does not fail
the request.
⚠ Not a clinical diagnosis
The CDRI is a risk indicator, not a diagnosis, prognosis, or
benefits-determination verdict. The score and phenotype are
statistical summaries intended to support decisions; they do not
make those decisions. Integrations built on /cdri/compute are
responsible for their own clinical governance and for communicating
the indicator's nature to end users.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/cdri/compute |
| Full URL | https://engine.tripod-health.com/api/cdri/compute |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No |
| Rate-limited | Yes |
There is no ?depth= query parameter on this endpoint; it always
produces its full response. Sending ?depth= is ignored.
Why this endpoint uses POST
/cdri/compute uses POST because it accepts a large, structured
clinical payload in the request body and returns a composite analysis.
GET is unsuitable: query strings have implementation-dependent length
limits that cannot accommodate the nested instruments map, and GET
is not designed to carry a JSON request body. The body is processed in
memory and is de-identified at the boundary (see
De-identification requirement).
Authentication
Send a valid API key in the apikey header. See
authentication.md.
Request
Headers
| Header | Required | Description |
|---|---|---|
apikey | Yes | Consumer API key. |
Content-Type | Yes (for the body) | Must be application/json. |
X-Request-ID | No | Optional correlation ID, up to 128 characters. Echoed in the response body's request_id and the X-Request-ID response header. |
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
recommend | string | No | rich | Controls the recommendations layer. One of rules, simple, rich, or off. See Recommendations modes. |
audience | string | No | (unset) | Steers the audience-framed narrative text (ai_summary and per-item message) toward a specific reader. One of employee, employer, provider. When unset, narrative output stays null regardless of ?recommend=. Deterministic fields are unaffected. See ?audience=. |
Body
{
"instruments": {
"orebro": {
"responses": {
"item_1": 6, "item_2": 7, "item_3": 6, "item_4": 5, "item_5": 5,
"item_6": 6, "item_7": 6, "item_8": 5, "item_9": 4, "item_10": 6,
"item_11": 4, "item_12": 4, "item_13": 6, "item_14": 5, "item_15": 6,
"item_16": 4, "item_17": 4, "item_18": 6, "item_19": 5, "item_20": 5,
"item_21": 6
}
},
"oswestry": {
"responses": {
"item_1": 3, "item_2": 4, "item_3": 4, "item_4": 3, "item_5": 4,
"item_6": 3, "item_7": 3, "item_8": 2, "item_9": 3, "item_10": 4
}
},
"pain_scale": { "responses": { "item_1": 7 } }
},
"demographics": {
"age_bracket": "45-54",
"sex": "male",
"occupation_class": "construction",
"region": "OH"
}
}
Body fields
| Field | Type | Required | Description |
|---|---|---|---|
instruments | object | No | Map of instrument_id → { "responses": { ... } }. May be omitted, empty, or carry any subset of the 14 scoreable instruments. |
instruments.<id>.responses | object | When <id> is present | Per-item response map. Same shape as the responses field on /score. |
demographics | object | null | No | Structural descriptors, identical in shape to the demographics block in /score. Improves cohort context and phenotype classification when supplied. |
situation | object | null | No | De-identified situational descriptors that turn on the embedded acuity block: age_bracket, injury_type, injury_region. See Embedded acuity view. |
onset | string | null | No | Injury/onset timestamp (e.g. "2026-03-09"). Used only to derive a duration for the acuity view; the raw date is discarded at the boundary and never persisted. See De-identification requirement. |
Any instrument name in the instruments map must match a registered
resource name — for example orebro, depression_screen,
opioid_risk (see /api/catalog/resources).
An unknown instrument name fails the request with 400 ValidationError
even though /cdri/compute is otherwise permissive.
Partial instrument responses
If an instrument is present but its responses map is missing one or
more items that the instrument's rubric marks as required (see the
per-instrument docs), the instrument is silently skipped — no error,
no 400. It contributes nothing to the result, and confidence is
derated accordingly. The public response does not surface which
instruments were skipped. Callers who need to distinguish "not sent"
from "sent but incomplete" should validate completeness client-side
against /questions before calling.
Demographics-only and empty bodies
Both of these are valid:
{ "demographics": { "age_bracket": "35-44", "sex": "female" } }
{}
Either produces a 200 response with an empty findings array, every
magnitude at 0.0, a cdri_score of 0.0, no red flags, and low
confidence. The phenotype classifier still runs and may produce a
phenotype from demographics alone, with low certainty under such
sparse input.
Sex-conditional scoring (ORT)
The Opioid Risk Tool uses sex-conditional item weights. Trinity reads
the condition value from instruments.opioid_risk.responses.sex, not
from demographics.sex. To score ORT with female weights under
/cdri/compute, include sex alongside the ORT item responses:
"instruments": {
"opioid_risk": {
"responses": {
"item_1": 0, "item_2": 0, "item_3": 1, "item_4": 0, "item_5": 1,
"sex": "female"
}
}
}
See instruments-opioid_risk.md. Omitting
sex from the ORT response map falls back to male-default weights,
which can inflate the result for female subjects.
De-identification requirement
All request bodies pass through Trinity's de-identification filter
before any business logic runs. A body containing an identifier-shaped
value or a blocked field name returns 400 DeIdentificationViolation.
The one exception is the onset (and optional as_of) timestamp: the
engine derives a duration from it at the boundary and discards the raw
date — a duration is not a Safe Harbor identifier, a date is. See
conventions.md.
Response
Success — 200 OK
Live capture against production with ?recommend=rules, using the
canonical body shown above (a high-severity subject across pain and
function). Values are real engine output, not illustrative.
{
"composite_id": "cdri",
"cdri_score": 83.3,
"magnitudes": {
"pain_dominant": 0.818,
"psychosocial_dominant": 0.8,
"fear_avoidance_dominant": 0.8,
"rtw_barrier": 0.8,
"multi_domain_catastrophic": 0.833,
"iatrogenic_escalation": 0.8,
"established_chronicity": 0.829
},
"overall_magnitude": 0.833,
"matrix_version": "2026-06-02-first-pass",
"findings": [
{
"algorithm_id": "orebro",
"algorithm_version": "1.0.0",
"score": 111,
"band": "high",
"raw": { "rubric_version": "1.0.0", "responded_items": 21 },
"context": {
"percentile": 70.83,
"cohort_key": "*",
"cohort_label": "Unconditioned (all respondents)",
"cohort_size": 1000,
"source": "placeholder"
},
"imputed": false,
"sd": null,
"prior_source": null
},
{
"algorithm_id": "oswestry",
"algorithm_version": "1.0.0",
"score": 66,
"band": "crippled",
"raw": { "rubric_version": "1.0.0", "responded_items": 10 },
"context": {
"percentile": 87.75,
"cohort_key": "*",
"cohort_label": "Unconditioned (all respondents)",
"cohort_size": 1000,
"source": "placeholder"
},
"imputed": false,
"sd": null,
"prior_source": null
},
{
"algorithm_id": "pain_scale",
"algorithm_version": "1.0.0",
"score": 7,
"band": "severe",
"raw": { "rubric_version": "1.0.0", "responded_items": 1 },
"context": {
"percentile": 87.86,
"cohort_key": "*",
"cohort_label": "Unconditioned (all respondents)",
"cohort_size": 1000,
"source": "placeholder"
},
"imputed": false,
"sd": null,
"prior_source": null
}
],
"phenotype": {
"backend": "deterministic",
"model_version": "2026-06-02-first-pass",
"phenotype": "multi_domain_catastrophic",
"distribution": {
"pain_dominant": 0.144,
"psychosocial_dominant": 0.1408,
"fear_avoidance_dominant": 0.1408,
"rtw_barrier": 0.1408,
"multi_domain_catastrophic": 0.1468,
"iatrogenic_escalation": 0.1408,
"established_chronicity": 0.146
},
"model_certainty": 0.1468,
"overall_magnitude": 0.833,
"trajectory": null,
"similar_cases": null,
"anomalies": null
},
"acuity": null,
"red_flags": [
{
"code": "complex_case_review",
"severity": "urgent",
"rule_id": "rec.escalation.multi_domain_catastrophic_top2",
"rule_version": "1.0.0",
"detail": "Phenotype 'multi_domain_catastrophic' appears in the top-two of the classifier's distribution. Top class 'multi_domain_catastrophic'; second class 'established_chronicity'.",
"triggers": {
"phenotype.top_name": "multi_domain_catastrophic",
"phenotype.second_name": "established_chronicity",
"phenotype.target_probability": 0.147
}
}
],
"confidence": {
"overall": 0.544,
"tier": "medium",
"breakdown": {
"input_completeness": 0.5,
"imputation_variance": 1,
"model_certainty": 0.1468
}
},
"recommendations": {
"engine_version": "1.0.0",
"ruleset_version": "1.0.0",
"disclaimer": "The Trinity Engine is a decision-support tool. Output is not medical advice, not a diagnosis, not a prescription, and not a benefits determination. The calling organization is responsible for clinical governance and for the interpretation and use of these suggestions.",
"ai_summary": null,
"items": [
{
"id": "r_d07dfb2281743263",
"rule_id": "rec.escalation.multi_domain_catastrophic_top2",
"rule_version": "1.0.0",
"priority": "urgent",
"category": "escalation",
"item_disclaimer": "Consider, not prescribe.",
"clinical_source": null,
"action_type": "escalate_complex_case_review",
"action": "Route this case to a complex-case review queue with an integrated biopsychosocial case-management pathway attached, and capture any domain instruments not yet administered so the next composite run disambiguates the multi-domain signal.",
"target": { "pathway": "complex_case_review" },
"rationale": "Phenotype 'multi_domain_catastrophic' appears in the top-two of the classifier's distribution.",
"triggers": {
"phenotype.top_name": "multi_domain_catastrophic",
"phenotype.second_name": "established_chronicity"
},
"expected_impact": null,
"followup": null,
"message": null
},
{
"id": "r_c15051de8f83bdcc",
"rule_id": "rec.data_acquisition.missing_dominant_domain_instrument",
"rule_version": "1.1.0",
"priority": "high",
"category": "data_acquisition",
"item_disclaimer": "Consider, not prescribe.",
"clinical_source": null,
"action_type": "administer_instruments",
"action": "Administer the missing function instruments before the next composite run: quick_dash, lower_extremity_function, neck_disability. This domain is driving the composite but is informed by only 1 of 4 inputs.",
"target": {
"instrument_ids": ["quick_dash", "lower_extremity_function", "neck_disability"]
},
"rationale": "The dominant domain is informed by only one of its available instruments.",
"triggers": {
"provided_instruments": ["oswestry"],
"missing_instruments": ["quick_dash", "lower_extremity_function", "neck_disability"]
},
"expected_impact": {
"metric": "confidence.breakdown.input_completeness",
"direction": "increase"
},
"followup": {
"type": "recompute",
"endpoint": "/api/cdri/compute",
"guidance": "Resubmit with the missing function instruments included."
},
"message": null
}
],
"suppressed": [],
"cardinality_cap": null
},
"depth": "full",
"catalog_version": "0.13.0",
"request_id": "docs-cdri-canonical",
"timestamp": "2026-06-03T04:21:41.812275+00:00"
}
Response fields
| Field | Type | Always present | Description |
|---|---|---|---|
composite_id | string | Yes | Always "cdri" on this endpoint. |
cdri_score | number | null | Yes | The overall risk headline, 0.0–100.0. null only under a pipeline failure that produces no composite — not reachable on a successful 200. |
magnitudes | object | Yes | Seven-key object, one severity (0.0–1.0) per recovery phenotype. A higher value means a stronger expression of that pattern. Every key is always present. |
overall_magnitude | number | null | Yes | The single strongest phenotype magnitude (0.0–1.0) — the peak the headline tracks. |
matrix_version | string | null | Yes | Opaque version tag for the active scoring configuration. May change between deploys; do not pin client logic to a specific value. |
findings | array | Yes | Zero-or-more Finding objects, one per scored instrument. See findings and patterns-score.md. |
phenotype | object | null | Yes | Phenotype block (see Phenotype block). Inner fields may be null if classification could not be produced. |
acuity | object | null | Yes | The embedded Risk Acuity view. Populated only when situation/onset were supplied; null otherwise. See Embedded acuity view. |
red_flags | array | Yes | Zero-or-more safety escalations (see Red flags). Independent of ?recommend= — present even under ?recommend=off. |
confidence | object | Yes | Confidence report (see Confidence). |
recommendations | object | null | Yes | Recommendations output, or null under ?recommend=off. Inner shape depends on the mode — see Recommendations modes. |
depth | string | Yes | Always "full" on this endpoint. |
catalog_version | string (SemVer) | Yes | Catalog version the deploy is serving. Compare with /api/version/get to detect mid-flight deploys. |
request_id | string | Yes | Correlation ID. Quote this in support tickets. |
timestamp | string (ISO 8601 UTC) | Yes | Server time when the response was emitted. |
magnitudes and cdri_score — scale note
magnitudes.*values are severities on a0.0–1.0scale:0.0= benign,1.0= most severe. Each key names a recovery phenotype.overall_magnitudeis the peak of those seven values.cdri_scoreis the0.0–100.0headline. It moves with the peak, so it is non-punitive: submitting more instruments cannot lower a subject's headline by dilution.
magnitudes and cdri_score are related but not interchangeable. A
psychosocial_dominant magnitude of 0.45 is not "45% psychosocial
disability"; it is a severity on the 0.0–1.0 scale.
Do not read a low cdri_score under partial input as "low overall
risk" — it may simply reflect domains for which no instruments were
submitted. Always read confidence alongside the score: a low
cdri_score paired with confidence.tier = "low" communicates "we did
not see enough to say much."
Phenotype block
| Field | Type | Description |
|---|---|---|
phenotype.backend | string | Opaque identifier for the backend that produced the block. "none" means classification could not be produced; treat any other value as "succeeded" and do not branch on the specific string. |
phenotype.model_version | string | null | Opaque version identifier. May change between deploys; do not pin client logic to it. null when backend is "none". |
phenotype.phenotype | string | null | One of the seven phenotype names (see taxonomy), or null if classification did not succeed. |
phenotype.distribution | object | null | A probability across all seven phenotypes, summing to 1.0 ± 0.01, or null. |
phenotype.model_certainty | number | null | Peak probability from distribution, 0.0–1.0, or null. |
phenotype.overall_magnitude | number | null | Echo of the top-level overall_magnitude. |
phenotype.trajectory / similar_cases / anomalies | null | Reserved for future releases. Always null today. |
Phenotype taxonomy (seven classes)
| Phenotype | One-line meaning |
|---|---|
pain_dominant | Pain intensity is the overriding driver of disability; psychosocial and functional signals are secondary. |
psychosocial_dominant | Mood, anxiety, or cognitive-emotional factors dominate the profile. |
fear_avoidance_dominant | Belief-based activity avoidance (fear of re-injury, kinesiophobia) drives the clinical picture. |
rtw_barrier | Workplace-specific barriers dominate (low recovery expectation, low work ability); other domains are comparatively intact. |
multi_domain_catastrophic | High severity across pain, psychosocial, function, and RTW — no single driver, global breakdown. |
iatrogenic_escalation | Elevated opioid-risk or treatment-side-effect markers alongside moderate pain/function signals. |
established_chronicity | Pattern consistent with long-duration established chronic pain (stable but severe multi-domain profile). |
phenotype.phenotype is the highest-probability class; its probability
is phenotype.model_certainty.
Phenotype degradation
If the classifier cannot produce a phenotype, the block is returned in
its degraded shape (backend: "none", inner fields null):
"phenotype": {
"backend": "none",
"model_version": null,
"phenotype": null,
"distribution": null,
"model_certainty": null,
"overall_magnitude": 0.833,
"trajectory": null,
"similar_cases": null,
"anomalies": null
}
confidence.breakdown.model_certainty is also null in this case. The
rest of the response (findings, magnitudes, cdri_score) is unaffected
and the HTTP status remains 200.
Findings
Each entry describes one instrument that scored. Fields a caller uses:
algorithm_id (the instrument), algorithm_version, score, band
(the qualitative severity label for that instrument), context (a
cohort-referenced percentile and cohort metadata), imputed, sd,
prior_source. The raw object is an internal per-instrument
diagnostic block; its contents are not a stable contract — do not parse
or depend on it. See patterns-score.md
for the full Finding shape.
Red flags
red_flags is a top-level array of safety escalations the engine
detected. It is independent of ?recommend= — escalations surface
even under ?recommend=off, so a caller can suppress recommendation
prose without losing safety signal. Each entry:
| Field | Type | Description |
|---|---|---|
code | string | Stable machine code for the escalation pathway (e.g. complex_case_review, suicide_risk_assessment). Branch on this. |
severity | string | urgent, high, medium, or routine. |
rule_id | string | Identifier of the rule that fired. |
rule_version | string (SemVer) | Version of that rule. |
detail | string | Human-readable explanation. Display, do not parse. |
triggers | object | The signals that fired the rule. Diagnostic; shape varies by rule. |
red_flags is [] when nothing escalates. Treat a populated array as a
prompt to route the case in your own workflow; the engine does not act
on the subject's behalf.
Embedded acuity view
When the request supplies situation (and an onset timestamp), the
response carries an acuity block — the same readout produced by the
standalone /api/acuity/compute. When
neither is supplied, acuity is null.
"acuity": {
"acuity_index": 55.37,
"priority_band": "expedited",
"components": {
"transition_risk": 0.818,
"situation_weight": 0.683,
"time_factor": 0.991
},
"weeks_since_onset": 12.31,
"time_known": true,
"matrix_version": "2026-06-02-first-pass"
}
acuity_index is 0–100; priority_band is one of immediate,
expedited, routine, low. weeks_since_onset is the duration the
engine derived from onset (the raw date is never persisted). See the
acuity endpoint doc for field-level
detail and the accepted situation values.
Confidence
confidence.overall is a 0.0–1.0 value. Tier cutoffs: < 0.4 =
low, < 0.75 = medium, ≥ 0.75 = high. confidence.breakdown
exposes diagnostic sub-signals (input_completeness,
imputation_variance, model_certainty) that are present when they
informed the overall value and null when they did not. Use the
breakdown to reason about why a tier landed where it did; do not try
to reproduce the rollup from it.
Recommendations modes
?recommend=<mode> controls the recommendations layer. The narrative
fields (ai_summary, per-item message) are additionally gated on
?audience=: without an audience, narrative output stays null.
| Mode | Default? | Output | When to use |
|---|---|---|---|
rich | Yes | recommendations populated; per-item message carries audience-tailored prose; ai_summary is a populated narrative object. | Caller renders a full audience-adapted UI. Highest latency. |
simple | No | recommendations populated; per-item message: null; ai_summary populated. | Block-level narrative alongside deterministic items. |
rules | No | recommendations populated; items[] carry deterministic action; message: null; ai_summary: null. | Fully deterministic output; the integrator drives their UI from action. |
off | No | recommendations: null. | Caller does not render recommendations. (red_flags is still present.) |
In the three non-off modes the recommendations object is always
present, even when no rules fire (items: []). A client parsing
recommendations.items must null-check first:
const items = response.recommendations?.items ?? [];
Each item carries two layers of text. The deterministic integrator
layer (action_type, action, rationale, target, triggers,
expected_impact, followup) is aimed at the integrating developer and
tells the tool what to do next. The audience-framed narrative layer
(message, and the block-level ai_summary) is a short line addressed
to the end user — populated only when the caller supplies ?audience=
with ?recommend=rich (per-item message) or ?recommend=simple
(block ai_summary).
priority values — urgent, high, medium, low — order items[];
urgent items sort to the top. suppressed[] lists rules that matched
but were held back. cardinality_cap describes any cap applied to the
number of returned items, or is null.
Callers displaying ai_summary or item message must show
recommendations.disclaimer alongside it.
?audience= — steering the narrative layer
?audience= tells the endpoint which end user the narrative text should
address. The deterministic item fields are identical across audiences;
only the narrative text differs.
| Value | Who it speaks to | Tone |
|---|---|---|
employee | the person being screened | Second-person ("you"). Plain, warm, no jargon. |
employer | an employer / HR reviewer | Third-person ("the employee"). Program / return-to-work focused. |
provider | a clinician | Third-person ("the individual"). Clinical terminology acceptable. |
?audience= is optional. When omitted, ai_summary and per-item
message stay null regardless of ?recommend= — without knowing the
reader, the narrative layer returns nothing rather than generic prose.
Invalid values (e.g. ?audience=patient) return 400 ValidationError.
Model-generated fields and non-determinism
The phenotype block, recommendations.ai_summary, and each
recommendations.items[i].message may be model-generated and can vary
across byte-identical calls. Do not cache by request-body hash and do
not assert on exact strings or probabilities in tests. Every other field
is produced deterministically and is stable across identical calls. When
a model-generated field cannot be produced, it degrades to null (or,
for phenotype, the degraded block) and the status stays 200.
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Composite produced. |
400 | ValidationError | Body not valid JSON, instruments contains an unknown id, an off-list situation value, or an invalid audience. |
400 | DeIdentificationViolation | Body contained an identifier-shaped value or a blocked field name. |
401 | Unauthenticated | Missing or invalid API key. |
413 | PayloadTooLarge | Body exceeds 512 KB. |
415 | UnsupportedMediaType | Content-Type is not application/json. |
429 | RateLimited | Per-consumer rate limit exceeded. |
500 | InternalError | Unexpected failure. Quote request_id. |
503 | ServiceUnavailable | A component is unhealthy. Retry with backoff. |
Examples
The snippets assume the call() helper from
quickstart.md.
curl
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/compute?recommend=rules" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instruments": {
"orebro": { "responses": { "item_1": 6, "item_2": 7, "item_3": 6, "item_4": 5, "item_5": 5, "item_6": 6, "item_7": 6, "item_8": 5, "item_9": 4, "item_10": 6, "item_11": 4, "item_12": 4, "item_13": 6, "item_14": 5, "item_15": 6, "item_16": 4, "item_17": 4, "item_18": 6, "item_19": 5, "item_20": 5, "item_21": 6 } },
"oswestry": { "responses": { "item_1": 3, "item_2": 4, "item_3": 4, "item_4": 3, "item_5": 4, "item_6": 3, "item_7": 3, "item_8": 2, "item_9": 3, "item_10": 4 } },
"pain_scale": { "responses": { "item_1": 7 } }
},
"demographics": { "age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH" }
}'
Python (requests)
# Uses the call() helper from quickstart.md.
result = call("POST", "/api/cdri/compute?recommend=rules", {
"instruments": {
"orebro": {"responses": {f"item_{i}": v for i, v in enumerate(
[6,7,6,5,5,6,6,5,4,6,4,4,6,5,6,4,4,6,5,5,6], start=1)}},
"oswestry": {"responses": {f"item_{i}": v for i, v in enumerate(
[3,4,4,3,4,3,3,2,3,4], start=1)}},
"pain_scale": {"responses": {"item_1": 7}},
},
"demographics": {
"age_bracket": "45-54", "sex": "male",
"occupation_class": "construction", "region": "OH",
},
})
print(f"CDRI {result['cdri_score']} ({result['confidence']['tier']})")
print(f"peak phenotype: {result['phenotype']['phenotype']}")
for dim, value in result["magnitudes"].items():
print(f" {dim:26} {value:.3f}")
for flag in result["red_flags"]:
print(f" [RED:{flag['severity']}] {flag['code']}")
Node.js (native fetch)
// Uses the call() helper from quickstart.md.
const result = await call("POST", "/api/cdri/compute?recommend=rules", body);
console.log(`CDRI ${result.cdri_score} (${result.confidence.tier})`);
console.log(`peak phenotype: ${result.phenotype?.phenotype}`);
for (const flag of result.red_flags) {
console.log(` [RED:${flag.severity}] ${flag.code}`);
}
for (const rec of result.recommendations?.items ?? []) {
console.log(` [${rec.priority}] ${rec.action}`);
}
TypeScript response interface
interface CdriResponse {
composite_id: "cdri";
cdri_score: number | null;
magnitudes: {
pain_dominant: number;
psychosocial_dominant: number;
fear_avoidance_dominant: number;
rtw_barrier: number;
multi_domain_catastrophic: number;
iatrogenic_escalation: number;
established_chronicity: number;
};
overall_magnitude: number | null;
matrix_version: string | null;
findings: Array<{
algorithm_id: string;
algorithm_version: string;
score: number;
band: string;
raw: Record<string, unknown>; // diagnostic; not a stable contract
context: null | {
percentile: number;
cohort_key: string;
cohort_label: string;
cohort_size: number;
source: string;
};
imputed: boolean;
sd: number | null;
prior_source: string | null;
}>;
phenotype: {
backend: string; // opaque; do not branch on the value
model_version: string | null; // opaque; do not pin
phenotype: string | null;
distribution: Record<string, number> | null;
model_certainty: number | null;
overall_magnitude: number | null;
trajectory: null;
similar_cases: null;
anomalies: null;
} | null;
acuity: null | {
acuity_index: number;
priority_band: "immediate" | "expedited" | "routine" | "low";
components: { transition_risk: number; situation_weight: number; time_factor: number };
weeks_since_onset: number | null;
time_known: boolean;
matrix_version: string;
};
red_flags: Array<{
code: string;
severity: "urgent" | "high" | "medium" | "routine";
rule_id: string;
rule_version: string;
detail: string;
triggers: Record<string, unknown>;
}>;
confidence: {
overall: number;
tier: "low" | "medium" | "high";
breakdown: {
input_completeness: number | null;
imputation_variance: number | null;
model_certainty: number | null;
};
};
recommendations: {
engine_version: string;
ruleset_version: string;
disclaimer: string;
ai_summary: null | { backend: string; model_version: string; audience: string; text: string };
items: Array<Record<string, unknown>>;
suppressed: Array<{ rule_id: string; reason: string; details: string }>;
cardinality_cap: null | { limit: number; applied: number };
} | null;
depth: "full";
catalog_version: string;
request_id: string;
timestamp: string;
}
Permissive-body and acuity variants
Demographics only
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/compute" \
-H "apikey: $ENGINE_API_KEY" -H "Content-Type: application/json" \
-d '{ "demographics": { "age_bracket": "35-44", "sex": "female" } }'
Returns 200 with cdri_score: 0.0, every magnitude 0.0,
red_flags: [], and confidence.tier: "low".
With the embedded acuity view
Add situation + onset to turn on the acuity block:
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/compute?recommend=rules" \
-H "apikey: $ENGINE_API_KEY" -H "Content-Type: application/json" \
-d '{
"instruments": { "oswestry": { "responses": { "item_1": 3, "item_2": 4, "item_3": 4, "item_4": 3, "item_5": 4, "item_6": 3, "item_7": 3, "item_8": 2, "item_9": 3, "item_10": 4 } }, "pain_scale": { "responses": { "item_1": 7 } } },
"demographics": { "age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH" },
"situation": { "age_bracket": "45_54", "injury_type": "disc_herniation", "injury_region": "low_back" },
"onset": "2026-03-09"
}'
The response is the standard CDRI shape with the acuity block
populated (see Embedded acuity view). Note the
situation.age_bracket value uses underscores (45_54), distinct from
the hyphenated demographics.age_bracket (45-54); see the
acuity doc for the accepted lists.
Error scenarios
Unknown instrument (400)
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/compute" \
-H "apikey: $ENGINE_API_KEY" -H "Content-Type: application/json" \
-H "X-Request-ID: docs-cdri-unknown" \
-d '{"instruments":{"frobnitz":{"responses":{"item_1":3}}}}' -w "\nHTTP %{http_code}\n"
Receive (400):
{
"timestamp": "2026-06-03T04:23:31.589Z",
"error": "ValidationError",
"message": "Unknown instrument: 'frobnitz'",
"request_id": "docs-cdri-unknown"
}
De-identification violation (400)
A blocked value anywhere in the body (here, an email address in a free-text field) is rejected before any scoring:
{
"timestamp": "2026-06-03T04:23:34.206Z",
"error": "DeIdentificationViolation",
"message": "demographics.note appears to contain an email address",
"field": "demographics.note",
"request_id": "docs-cdri-deid"
}
Intended use
- Cross-domain risk summary for a subject where the caller wants one number-plus-phenotype rollup from some-or-all of the instruments.
- Triage from minimal signal — send whatever is available, accept lower confidence.
- Time-of-risk prioritization — add
situation/onsetfor the embedded acuity view in the same call. - Time-series tracking — the same call structure works whether one or many instruments are present each time.
Mistaken use
- Do not compare
magnitudes.*tocdri_scoreon one scale. Magnitudes are0.0–1.0; CDRI is0.0–100.0. - Do not read a magnitude of
0.0as "low risk in that pattern." Under sparse input it may mean "no finding informed that phenotype." Readconfidencealongside. - Do not cache responses by request-body hash. Model-generated fields may differ across byte-identical calls.
- Do not treat
phenotype.phenotypeas a diagnosis. Display it alongsidemodel_certainty. - Do not omit
sexfrom the ORTresponsesmap for a female subject — see instruments-opioid_risk.md. - Do not depend on
findings[].raw,matrix_version,phenotype.backend, orphenotype.model_version— these are opaque/diagnostic and may change between deploys. - Do not send more than 512 KB of request body.
Debugging
- Capture
request_id— present in every response body and theX-Request-IDresponse header. 400"Unknown instrument" — a name ininstrumentsis not in/api/catalog/resources.400onsituation— anage_bracket/injury_type/injury_regionvalue is off-list; the errorfieldnames which. See the acuity doc.cdri_scoreis0.0for a subject you know is impaired — no instrument scored. Either the body had no instruments, or every submitted instrument was below its required-items bar and got silently skipped. Validate completeness against/questions.phenotype.phenotypeisnull— the classifier degraded (backend: "none"). The rest of the response is valid; retry once if phenotype matters.confidence.tieris"low"on complete input — a low-certainty classification or thin reference cohorts. This is a signal about uncertainty, not a validation error.
Performance and caching
/cdri/compute does more work per call than /score and typically
takes a little longer. If you need repeat access to a result, cache it
on your own subject-correlation key. Byte-identical responses are
not guaranteed because model-generated fields may vary.
Rate limits
See rate-limits.md.
Related
- composite-cdri-plan.md — the value-of-information inverse: same request body, but returns which questions to ask next and how much confidence each buys, instead of the score.
- composite-magnitude-compute.md — the standalone "how much" readout (magnitudes only).
- composite-acuity-compute.md — the standalone "how urgent" readout (priority of risk against time).
- composite-phenotype-classify.md — phenotype + recommendations only, no scoring envelope.
- patterns-questions.md — fetch item lists before submitting.
- patterns-score.md — the strict single-instrument counterpart.
- authentication.md, errors.md, rate-limits.md, conventions.md — cross-cutting contract rules.
Change history
| Date | Change |
|---|---|
| 2026-04-20 | Initial publication. |
| 2026-06-03 | Reshaped to the seven-phenotype magnitudes model with a non-punitive peak cdri_score; added top-level red_flags, the embedded acuity view, overall_magnitude/matrix_version; removed the tpa audience. |