Loading documentation

Access required

Your account doesn't have access to the documentation. Contact your administrator.

Sign out
Skip to main content

Composite Disability Risk Index

Status: stable Category: composite Added: 2026-04-20 Last 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 (0100) — the headline overall disability-related risk indicator.
  • A magnitudes block: seven phenotype-named severities (0.01.0), one per recovery phenotype, plus an overall_magnitude.
  • A phenotype block attaching one of seven recovery phenotypes, with a distribution across all seven and a certainty.
  • A findings array carrying every instrument that actually scored.
  • A red_flags array surfacing any safety escalations detected.
  • A confidence report the caller can use to decide how much weight to put on the result.
  • Optionally, an acuity block (priority-of-risk-against-time) when the caller also supplies de-identified situation and onset inputs.

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

FieldValue
MethodPOST
Path/api/cdri/compute
Full URLhttps://engine.tripod-health.com/api/cdri/compute
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo
Rate-limitedYes

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

HeaderRequiredDescription
apikeyYesConsumer API key.
Content-TypeYes (for the body)Must be application/json.
X-Request-IDNoOptional correlation ID, up to 128 characters. Echoed in the response body's request_id and the X-Request-ID response header.

Query parameters

ParameterTypeRequiredDefaultDescription
recommendstringNorichControls the recommendations layer. One of rules, simple, rich, or off. See Recommendations modes.
audiencestringNo(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

FieldTypeRequiredDescription
instrumentsobjectNoMap of instrument_id{ "responses": { ... } }. May be omitted, empty, or carry any subset of the 14 scoreable instruments.
instruments.<id>.responsesobjectWhen <id> is presentPer-item response map. Same shape as the responses field on /score.
demographicsobject | nullNoStructural descriptors, identical in shape to the demographics block in /score. Improves cohort context and phenotype classification when supplied.
situationobject | nullNoDe-identified situational descriptors that turn on the embedded acuity block: age_bracket, injury_type, injury_region. See Embedded acuity view.
onsetstring | nullNoInjury/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

FieldTypeAlways presentDescription
composite_idstringYesAlways "cdri" on this endpoint.
cdri_scorenumber | nullYesThe overall risk headline, 0.0100.0. null only under a pipeline failure that produces no composite — not reachable on a successful 200.
magnitudesobjectYesSeven-key object, one severity (0.01.0) per recovery phenotype. A higher value means a stronger expression of that pattern. Every key is always present.
overall_magnitudenumber | nullYesThe single strongest phenotype magnitude (0.01.0) — the peak the headline tracks.
matrix_versionstring | nullYesOpaque version tag for the active scoring configuration. May change between deploys; do not pin client logic to a specific value.
findingsarrayYesZero-or-more Finding objects, one per scored instrument. See findings and patterns-score.md.
phenotypeobject | nullYesPhenotype block (see Phenotype block). Inner fields may be null if classification could not be produced.
acuityobject | nullYesThe embedded Risk Acuity view. Populated only when situation/onset were supplied; null otherwise. See Embedded acuity view.
red_flagsarrayYesZero-or-more safety escalations (see Red flags). Independent of ?recommend= — present even under ?recommend=off.
confidenceobjectYesConfidence report (see Confidence).
recommendationsobject | nullYesRecommendations output, or null under ?recommend=off. Inner shape depends on the mode — see Recommendations modes.
depthstringYesAlways "full" on this endpoint.
catalog_versionstring (SemVer)YesCatalog version the deploy is serving. Compare with /api/version/get to detect mid-flight deploys.
request_idstringYesCorrelation ID. Quote this in support tickets.
timestampstring (ISO 8601 UTC)YesServer time when the response was emitted.

magnitudes and cdri_score — scale note

  • magnitudes.* values are severities on a 0.01.0 scale: 0.0 = benign, 1.0 = most severe. Each key names a recovery phenotype.
  • overall_magnitude is the peak of those seven values.
  • cdri_score is the 0.0100.0 headline. 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.01.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

FieldTypeDescription
phenotype.backendstringOpaque 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_versionstring | nullOpaque version identifier. May change between deploys; do not pin client logic to it. null when backend is "none".
phenotype.phenotypestring | nullOne of the seven phenotype names (see taxonomy), or null if classification did not succeed.
phenotype.distributionobject | nullA probability across all seven phenotypes, summing to 1.0 ± 0.01, or null.
phenotype.model_certaintynumber | nullPeak probability from distribution, 0.01.0, or null.
phenotype.overall_magnitudenumber | nullEcho of the top-level overall_magnitude.
phenotype.trajectory / similar_cases / anomaliesnullReserved for future releases. Always null today.

Phenotype taxonomy (seven classes)

PhenotypeOne-line meaning
pain_dominantPain intensity is the overriding driver of disability; psychosocial and functional signals are secondary.
psychosocial_dominantMood, anxiety, or cognitive-emotional factors dominate the profile.
fear_avoidance_dominantBelief-based activity avoidance (fear of re-injury, kinesiophobia) drives the clinical picture.
rtw_barrierWorkplace-specific barriers dominate (low recovery expectation, low work ability); other domains are comparatively intact.
multi_domain_catastrophicHigh severity across pain, psychosocial, function, and RTW — no single driver, global breakdown.
iatrogenic_escalationElevated opioid-risk or treatment-side-effect markers alongside moderate pain/function signals.
established_chronicityPattern 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:

FieldTypeDescription
codestringStable machine code for the escalation pathway (e.g. complex_case_review, suicide_risk_assessment). Branch on this.
severitystringurgent, high, medium, or routine.
rule_idstringIdentifier of the rule that fired.
rule_versionstring (SemVer)Version of that rule.
detailstringHuman-readable explanation. Display, do not parse.
triggersobjectThe 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 0100; 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.01.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.

ModeDefault?OutputWhen to use
richYesrecommendations populated; per-item message carries audience-tailored prose; ai_summary is a populated narrative object.Caller renders a full audience-adapted UI. Highest latency.
simpleNorecommendations populated; per-item message: null; ai_summary populated.Block-level narrative alongside deterministic items.
rulesNorecommendations populated; items[] carry deterministic action; message: null; ai_summary: null.Fully deterministic output; the integrator drives their UI from action.
offNorecommendations: 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.

ValueWho it speaks toTone
employeethe person being screenedSecond-person ("you"). Plain, warm, no jargon.
employeran employer / HR reviewerThird-person ("the employee"). Program / return-to-work focused.
providera clinicianThird-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

CodeMeaningWhen
200OKComposite produced.
400ValidationErrorBody not valid JSON, instruments contains an unknown id, an off-list situation value, or an invalid audience.
400DeIdentificationViolationBody contained an identifier-shaped value or a blocked field name.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds 512 KB.
415UnsupportedMediaTypeContent-Type is not application/json.
429RateLimitedPer-consumer rate limit exceeded.
500InternalErrorUnexpected failure. Quote request_id.
503ServiceUnavailableA 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/onset for 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.* to cdri_score on one scale. Magnitudes are 0.01.0; CDRI is 0.0100.0.
  • Do not read a magnitude of 0.0 as "low risk in that pattern." Under sparse input it may mean "no finding informed that phenotype." Read confidence alongside.
  • Do not cache responses by request-body hash. Model-generated fields may differ across byte-identical calls.
  • Do not treat phenotype.phenotype as a diagnosis. Display it alongside model_certainty.
  • Do not omit sex from the ORT responses map for a female subject — see instruments-opioid_risk.md.
  • Do not depend on findings[].raw, matrix_version, phenotype.backend, or phenotype.model_version — these are opaque/diagnostic and may change between deploys.
  • Do not send more than 512 KB of request body.

Debugging

  1. Capture request_id — present in every response body and the X-Request-ID response header.
  2. 400 "Unknown instrument" — a name in instruments is not in /api/catalog/resources.
  3. 400 on situation — an age_bracket/injury_type/injury_region value is off-list; the error field names which. See the acuity doc.
  4. cdri_score is 0.0 for 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.
  5. phenotype.phenotype is null — the classifier degraded (backend: "none"). The rest of the response is valid; retry once if phenotype matters.
  6. confidence.tier is "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.

Change history

DateChange
2026-04-20Initial publication.
2026-06-03Reshaped 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.