Loading documentation

Access required

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

Sign out
Skip to main content

Single-instrument scoring

Status: stable Category: patterns Added: 2026-04-20 Last reviewed: 2026-06-03

Summary

The shared contract for every POST /api/:category/:resource/score endpoint. Each instrument exposes a /score operation that accepts an item-response body, runs the analytical pipeline at the requested ?depth=, and returns a finding plus a confidence report.

Per-instrument docs (instruments-<resource>.md) list what is unique to that instrument: item map, scoring method, band cutoffs, worked example. They defer to this document for everything else.

For the flagship composite (/api/cdri/compute), which accepts multiple instruments in one call and uses a permissive input posture, see composite-cdri-compute.md.

Endpoint shape

FieldValue
MethodPOST
Path template/api/:category/:resource/score
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo
Rate-limitedYes

:category and :resource must match a registered pair in the catalog. See GET /api/catalog/resources.

Purpose

  • Score one instrument for one subject. Send responses, get back a score, band, and confidence.
  • Opt into richer output via ?depth=. ?depth=basic (default) returns the instrument score and band. ?depth=contextual adds population-percentile context when a matching reference cohort exists. ?depth=full additionally enables the recommendations layer.
  • Strict input validation. If the body does not include every required item for the instrument, the request fails with 400 ValidationError and a missing_items list. No partial scoring here — that is /cdri/compute's job.

Authentication

Send a valid API key in the apikey header. See authentication.md.

Request

Headers

HeaderRequiredDescription
apikeyYesConsumer API key.
Content-TypeYesMust 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.

Path parameters

ParameterTypeDescription
categorystringCatalog category.
resourcestringCatalog resource within that category.

Query parameters

ParameterTypeRequiredDefaultDescription
depthstringNobasicOne of basic, contextual, full. Controls how much detail the response carries. See the depth table below.
recommendstringNorichControls the recommendations layer. One of rules, simple, rich, or off. The recommendations layer runs only when depth=full; at lower depths this parameter is accepted but recommendations is null. See ?recommend= modes on /cdri/compute.
audiencestringNo(unset)Steers the audience-framed narrative text (ai_summary + per-item message) toward a specific reader. One of employee, employer, provider. When unset, narrative output stays null. See ?audience= on /cdri/compute.

Body

{
"responses": {
"item_1": 2,
"item_2": 3
},
"demographics": {
"age_bracket": "35-44",
"sex": "female",
"occupation_class": "healthcare",
"naics_code": "621111",
"region": "TX"
}
}

Body fields

FieldTypeRequiredDescription
responsesobjectYesMap of item_id → numeric value. Keys match the items[].id values from /questions; values match one of the answer_choices[].value entries.
demographicsobject | nullNoStructural descriptors that improve cohort matching and classification when supplied. Ignored at ?depth=basic.
demographics.age_bracketstringNoCoarse age band (for example "25-34", "35-44"). Never a raw DOB or year.
demographics.sexstringNoSelf-reported categorical: "female", "male", "nonbinary", "undisclosed". Some instruments (notably ORT) use this to select a scoring weight table — document explicitly in the per-instrument doc.
demographics.occupation_classstringNoOccupation-class token (for example "retail", "manufacturing", "healthcare").
demographics.naics_codestringNoSix-digit NAICS industry code, as a string.
demographics.regionstringNoUS state or region code (for example "TX", "northeast"). Never a street address.

responses is the only required field. Every item listed in the instrument's required_items (from /questions) must appear as a key in responses — otherwise the request fails with 400 ValidationError and a missing_items list.

De-identification requirement

The standard de-identification check runs on every request body. Any identifier-shaped value or blocked field name fails with 400 DeIdentificationViolation before the pipeline runs. See conventions.md.

Rejected payload shapes

  • An empty body — responses is required.
  • A body that sends responses at the top level instead of under responses ({ "item_1": 4 } rather than { "responses": { "item_1": 4 } }).
  • A body with extra top-level keys beyond responses and demographics. Pydantic extra="forbid" rejects unknowns with 400 ValidationError.

The ?depth= query parameter

ValueConfidence terms populatedResponse adds
basic (default)input_completeness onlyScore + band + confidence.
contextualinput_completeness, imputation_varianceAdds finding.context (percentile, cohort) when a matching reference cohort exists.
fullinput_completeness, imputation_variance, model_certaintyContextual, plus the recommendations block (rules-driven items and, when ?audience= is set, audience-framed narrative).

Increasing depth does not change the score or band. It adds context around the score. Your integration can start at basic and layer up without refactoring response parsing.

Response

Success — 200 OK (depth=basic)

{
"instrument": "pain_scale",
"category": "pain",
"finding": {
"algorithm_id": "pain_scale",
"algorithm_version": "1.0.0",
"score": 6.0,
"band": "moderate",
"raw": {
"rubric_version": "1.0.0",
"responded_items": 1
},
"context": null,
"imputed": false,
"sd": null,
"prior_source": null
},
"confidence": {
"overall": 1.0,
"tier": "high",
"breakdown": {
"input_completeness": 1.0,
"imputation_variance": null,
"model_certainty": null
}
},
"depth": "basic",
"catalog_version": "0.13.0",
"request_id": "8f2c3b1d-5e4a-4a9c-9b1e-2a7f1d3e4c5b",
"timestamp": "2026-04-20T17:32:10.812Z"
}

Success — 200 OK (depth=contextual)

Adds finding.context when the pipeline finds a matching cohort. Other fields are identical.

{
"instrument": "pain_scale",
"category": "pain",
"finding": {
"algorithm_id": "pain_scale",
"algorithm_version": "1.0.0",
"score": 6.0,
"band": "moderate",
"raw": { "rubric_version": "1.0.0", "responded_items": 1 },
"context": {
"percentile": 0.74,
"cohort_key": "age:35-44|occ:healthcare",
"cohort_label": "Healthcare, 35-44",
"cohort_size": 3200,
"source": "literature:2023"
},
"imputed": false,
"sd": null,
"prior_source": null
},
"confidence": {
"overall": 0.88,
"tier": "high",
"breakdown": {
"input_completeness": 1.0,
"imputation_variance": 1.0,
"model_certainty": null
}
},
"depth": "contextual",
"catalog_version": "0.13.0",
"request_id": "...",
"timestamp": "..."
}

Success — 200 OK (depth=full)

Adds model_certainty to the confidence breakdown and populates the recommendations block. Unlike /cdri/compute, the /score response does not attach a phenotype block — the flagship composite is the right surface for phenotype classification because phenotype reasoning depends on cross-domain signals.

The recommendations block follows the same shape and the same ?recommend=rules|simple|rich|off and ?audience=employee|employer|provider semantics as on /cdri/compute. Narrative output (ai_summary and per-item message) is gated on ?audience= being supplied. Without an audience, narrative output stays null regardless of ?recommend=; the deterministic action field on every item is always populated. See composite-cdri-compute.md#model-generated-fields-and-non-determinism for when ai_summary is populated, how it degrades, and the non-determinism caveat.

Abbreviated shape (rest of the fields mirror the basic / contextual examples above):

{
"instrument": "pain_scale",
"category": "pain",
"confidence": {
"overall": 0.87,
"tier": "high",
"breakdown": {
"input_completeness": 1.0,
"imputation_variance": 1.0,
"model_certainty": 0.72
}
},
"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": {
"backend": "anthropic",
"model_version": "claude-haiku-4-5-20251001",
"audience": "provider",
"text": "The individual's pain rating falls in the moderate band. Pairing this result with a function-domain instrument (Oswestry, QuickDASH, or LEFS as appropriate) on the next encounter would support a more complete picture of the presentation."
},
"items": [],
"suppressed": [],
"cardinality_cap": null
},
"depth": "full",
"catalog_version": "0.13.0",
"request_id": "...",
"timestamp": "..."
}

At ?recommend=rules (default), ai_summary is null and the items[] list contains only deterministic rule-pack output. At ?recommend=off, the whole recommendations field is null. See composite-cdri-compute.md#recommendations-modes.

Response fields

FieldTypeAlways presentDescription
instrumentstringYesResource name matching the URL's :resource.
categorystringYesCategory matching the URL's :category.
findingobject | nullYesThe scoring output. null should not occur under normal operation; present-but-empty only if the rubric and scoring layer disagreed catastrophically.
finding.algorithm_idstringYesIdentifier for the scoring algorithm that produced this finding.
finding.algorithm_versionstring (SemVer)YesAlgorithm version. Independent of rubric version; bumps when math shape changes.
finding.scorenumberYesScalar score. Range is instrument-specific.
finding.bandstringYesBand name (for example "low", "moderate", "high") as defined by the instrument rubric. Never translated for audience; this is a scoring category, not a display label.
finding.rawobjectYesOpaque diagnostic metadata (for example rubric_version, responded_items). Contents are not a stable contract and may change between deploys — display if useful, but do not parse or depend on specific keys.
finding.contextobject | nullYesCohort-percentile context. null at depth=basic, and also null at deeper depths when no matching reference cohort exists for this score.
finding.context.percentilenumberWhen context != nullPercentile of this score within the cohort (0.01.0).
finding.context.cohort_keystringWhen context != nullCanonical cohort identifier Trinity matched on.
finding.context.cohort_labelstringWhen context != nullHuman-readable cohort label.
finding.context.cohort_sizeintegerWhen context != nullNumber of reference observations in the cohort. Small cohorts (< 500) signal a less reliable percentile — callers can derate accordingly.
finding.context.sourcestringWhen context != nullWhere the distribution came from (for example "literature:2023", "corpus:2026-03").
finding.imputedbooleanYesAlways false on /score responses — imputation only happens in /cdri/compute.
finding.sdnumber | nullYesImputation standard deviation (null on /score).
finding.prior_sourcestring | nullYesWhere imputation priors came from (null on /score).
confidence.overallnumberYesOverall confidence, 0.01.0.
confidence.tierstringYesThreshold classification: "low" (< 0.4), "medium" (0.4 – 0.75), "high" (≥ 0.75).
confidence.breakdown.input_completenessnumber | nullYes1.0 on a successful strict /score (every required item was present) or null if the term did not participate.
confidence.breakdown.imputation_variancenumber | nullYesnull at depth=basic; populated at contextual / full as a proxy for cohort-context quality (higher is better).
confidence.breakdown.model_certaintynumber | nullYesnull unless depth=full and classification produced a usable distribution.
recommendationsobject | nullYesRecommendations output. null at ?depth=basic and ?depth=contextual. Populated at ?depth=full when at least one rule fires — same shape as on /cdri/compute, including ai_summary when the caller uses ?recommend=simple or ?recommend=rich. See composite-cdri-compute.md#recommendations-modes for the full block shape and composite-cdri-compute.md#model-generated-fields-and-non-determinism for when model-generated fields populate and when they degrade.
depthstringYesThe effective depth: "basic", "contextual", or "full". Matches what you sent, or the default if you omitted the query.
catalog_versionstring (SemVer)YesCatalog version of the deploy that scored this request.
request_idstringYesCorrelation ID. Matches the X-Request-ID response header.
timestampstring (ISO 8601 UTC)YesServer time when the response was emitted. Carries microsecond precision and an explicit +00:00 offset (e.g. "2026-04-21T23:09:27.554373+00:00").

How to read confidence

confidence.overall is a 0.01.0 rollup. Use confidence.tier (low / medium / high) for quick branching; use confidence.breakdown to reason about why a tier landed where it did. Terms that did not participate for a given call are null in the breakdown and do not contribute to overall. At depth=basic, only input_completeness is populated; at contextual, imputation_variance is added; at full, model_certainty is added.

Status codes

CodeMeaningWhen
200OKScored.
400ValidationErrorBody shape invalid, required items missing, or ?depth= value unknown. Extras: field (typically "responses", "depth", or an item id); missing_items and instrument when items are missing.
400DeIdentificationViolationBody contains an identifier. See conventions.md.
401UnauthenticatedMissing or invalid API key.
404NotFound(category, resource) pair is not registered.
413PayloadTooLargeBody exceeds 512 KB.
415UnsupportedMediaTypeContent-Type is not application/json.
429RateLimitedPer-consumer rate limit exceeded.
500InternalErrorUnexpected failure. Quote request_id in the ticket.
503ServiceUnavailableService is temporarily unhealthy. Retry with backoff.

The missing-items error

When responses is missing any id in the instrument's required_items, Trinity rejects with:

{
"timestamp": "2026-04-20T17:32:10.812Z",
"error": "ValidationError",
"message": "Missing required items for 'pain_scale'",
"request_id": "...",
"missing_items": ["item_1"],
"instrument": "pain_scale"
}

missing_items is an array of the item IDs that were expected but absent. Render this back to your user so they can complete the form.

Examples

The language-specific invocations below assume the call() helper (or its equivalent) from quickstart.md. Per-instrument docs (for example instruments-orebro.md) carry their own worked-example pairs with the full item set for that instrument; this section shows the shared pattern.

1. Canonical request and response (basic depth)

Send:

POST /api/pain/pain_scale/score HTTP/1.1
Host: engine.tripod-health.com
apikey: $ENGINE_API_KEY
Content-Type: application/json
X-Request-ID: docs-pain_scale-score-basic

{"responses":{"item_1":6}}

Receive (200 OK):

{
"instrument": "pain_scale",
"category": "pain",
"finding": {
"algorithm_id": "pain_scale",
"algorithm_version": "1.0.0",
"score": 6,
"band": "moderate",
"raw": {
"rubric_version": "1.0.0",
"responded_items": 1
},
"context": null,
"imputed": false,
"sd": null,
"prior_source": null
},
"confidence": {
"overall": 1,
"tier": "high",
"breakdown": {
"input_completeness": 1,
"imputation_variance": null,
"model_certainty": null
}
},
"recommendations": null,
"depth": "basic",
"catalog_version": "0.13.0",
"request_id": "docs-pain_scale-score-basic",
"timestamp": "2026-04-21T23:09:27.554373+00:00"
}

2. Request and response with demographics (contextual depth)

Send:

POST /api/pain/pain_scale/score?depth=contextual HTTP/1.1
Host: engine.tripod-health.com
apikey: $ENGINE_API_KEY
Content-Type: application/json
X-Request-ID: docs-pain_scale-score-contextual

{
"responses": { "item_1": 6 },
"demographics": {
"age_bracket": "35-44",
"occupation_class": "healthcare",
"region": "TX"
}
}

Receive (200 OK): identical shape as the basic example above, with finding.context populated:

"finding": {
"algorithm_id": "pain_scale",
"algorithm_version": "1.0.0",
"score": 6,
"band": "moderate",
"raw": { "rubric_version": "1.0.0", "responded_items": 1 },
"context": {
"percentile": 85,
"cohort_key": "*",
"cohort_label": "Unconditioned (all respondents)",
"cohort_size": 1000,
"source": "placeholder"
},
"imputed": false,
"sd": null,
"prior_source": null
}

confidence.breakdown.imputation_variance becomes 1 (rather than null) when contextualization succeeded.

3. Calling the endpoint in your language

curl

# Basic
curl -sS -X POST "https://engine.tripod-health.com/api/pain/pain_scale/score" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"responses":{"item_1":6}}'

# Contextual with demographics
curl -sS -X POST "https://engine.tripod-health.com/api/pain/pain_scale/score?depth=contextual" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"responses": { "item_1": 6 },
"demographics": { "age_bracket": "35-44", "occupation_class": "healthcare" }
}'

Python (requests)

# Uses the call() helper from quickstart.md.
def score(category: str, resource: str, responses: dict,
demographics: dict | None = None, depth: str = "basic") -> dict:
path = f"/api/{category}/{resource}/score?depth={depth}"
body = {"responses": responses, "demographics": demographics}
return call("POST", path, body)

result = score(
"function", "oswestry",
responses={f"item_{i}": 3 for i in range(1, 11)},
demographics={"age_bracket": "35-44", "occupation_class": "manufacturing"},
depth="contextual",
)
print(f"{result['instrument']} -> {result['finding']['score']} ({result['finding']['band']})")

Node.js (native fetch)

// Uses the call() helper from quickstart.md.
async function score(category, resource, responses, demographics, depth = "basic") {
return call(
"POST",
`/api/${category}/${resource}/score?depth=${depth}`,
{ responses, demographics },
);
}

const result = await score(
"pain", "pain_scale",
{ item_1: 6 },
{ age_bracket: "35-44", occupation_class: "healthcare" },
"contextual",
);
console.log(`${result.instrument} -> ${result.finding.score} (${result.finding.band})`);

TypeScript

// Uses the typed call<T>() helper from quickstart.md.
interface Finding {
algorithm_id: string;
algorithm_version: string;
score: number;
band: string;
raw: Record<string, unknown>;
context: null | {
percentile: number;
cohort_key: string;
cohort_label: string;
cohort_size: number;
source: string;
};
imputed: boolean;
sd: number | null;
prior_source: string | null;
}

interface ScoreResponse {
instrument: string;
category: string;
finding: Finding | null;
confidence: {
overall: number;
tier: "low" | "medium" | "high";
breakdown: {
input_completeness: number | null;
imputation_variance: number | null;
model_certainty: number | null;
};
};
// `null` at depth=basic and depth=contextual.
// At depth=full, reuses the CdriResponse["recommendations"] shape -- see
// composite-cdri-compute.md for the full type definition.
recommendations: null | {
engine_version: string;
ruleset_version: string;
disclaimer: string;
ai_summary: null | {
backend: string;
model_version: string;
audience: "employee" | "employer" | "provider";
text: string;
};
items: Array<Record<string, unknown>>;
suppressed: Array<{ rule_id: string; reason: string; details: string }>;
cardinality_cap: null | { limit: number; applied: number };
};
depth: "basic" | "contextual" | "full";
catalog_version: string;
request_id: string;
timestamp: string;
}

const result = await call<ScoreResponse>(
"POST",
"/api/pain/pain_scale/score",
{ responses: { item_1: 6 } },
);

Go

// Uses the call() helper from quickstart.md.
body := map[string]any{
"responses": map[string]int{"item_1": 6},
"demographics": map[string]string{
"age_bracket": "35-44",
"occupation_class": "healthcare",
},
}
result, err := call("POST", "/api/pain/pain_scale/score?depth=contextual", body, 3)
if err != nil {
panic(err)
}
finding := result["finding"].(map[string]any)
fmt.Printf("%v -> %v (%v)\n",
result["instrument"], finding["score"], finding["band"])

Java

// Uses the call() helper from quickstart.md.
// For real code, build the body with Jackson/Gson rather than string
// concatenation; the minimal example below is readable not production.
String body = "{\"responses\":{\"item_1\":6}}";
String result = call("POST", "/api/pain/pain_scale/score", body, 3);
System.out.println("instrument: " + extract(result, "instrument"));

C# (.NET HttpClient)

// Uses the Call() helper from quickstart.md.
var result = await Call(HttpMethod.Post, "/api/pain/pain_scale/score?depth=contextual",
new {
responses = new { item_1 = 6 },
demographics = new { age_bracket = "35-44", occupation_class = "healthcare" },
});

var finding = result.GetProperty("finding");
var score = finding.GetProperty("score").GetDouble();
var band = finding.GetProperty("band").GetString();
Console.WriteLine($"{result.GetProperty("instrument").GetString()} -> {score} ({band})");

PowerShell

# Uses the Invoke-Engine helper from quickstart.md.
$result = Invoke-Engine -Method Post -Path "/api/pain/pain_scale/score?depth=contextual" -Body @{
responses = @{ item_1 = 6 }
demographics = @{ age_bracket = "35-44"; occupation_class = "healthcare" }
}
"$($result.instrument) -> $($result.finding.score) ($($result.finding.band))"

4. Error scenarios

Missing required items

Send (depression_screen requires items 1–9 but we sent only item 1):

curl -sS -X POST "https://engine.tripod-health.com/api/psychosocial/depression_screen/score" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-score-missing-items" \
-d '{"responses":{"item_1":2}}' \
-w "\nHTTP %{http_code}\n"

Receive (400):

{
"timestamp": "2026-04-21T23:10:00.000Z",
"error": "ValidationError",
"message": "Missing required items for 'depression_screen'",
"missing_items": ["item_2","item_3","item_4","item_5","item_6","item_7","item_8","item_9"],
"instrument": "depression_screen",
"request_id": "docs-score-missing-items"
}

Invalid depth

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/pain/pain_scale/score?depth=deep" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-score-bad-depth" \
-d '{"responses":{"item_1":6}}' \
-w "\nHTTP %{http_code}\n"

Receive (400):

{
"timestamp": "2026-04-21T23:10:00.000Z",
"error": "ValidationError",
"message": "Unknown depth 'deep'. Valid values: basic, contextual, full.",
"field": "depth",
"request_id": "docs-score-bad-depth"
}

Intended use

  • Score a single instrument in isolation when the caller only needs one dimension of risk (for example, a pain-only screen).
  • Drive a one-instrument UI flow. The /questions endpoint populates the form; /score returns the result.
  • Feed an upstream pipeline that consumes instrument scores. Layer-1 score and band are the scalar contract.

Mistaken use

  • Do not use /score for cross-instrument scoring. Every /score call scores exactly one instrument. Use POST /api/cdri/compute for a cross-domain composite — one call, many instruments, permissive input.
  • Do not send partial responses here. /score is strict: missing any required_items entry fails the request. /cdri/compute is the permissive endpoint.
  • Do not retry a 400 ValidationError unchanged. The request will fail the same way. Fix the missing_items or field, then retry.
  • Do not cache a score response by request body. Identical inputs are served again on each call. Retry logic should handle 429 and 5xx only.
  • Do not treat confidence.overall as a diagnostic statement. Confidence reflects how much of the expected input was present and how well the supporting context matched — not clinical severity. Use finding.band for severity.

Debugging

  1. Capture request_id. Present in the response body and the X-Request-ID response header.
  2. 400 ValidationError with missing_items — you did not send every required item. Look up the current required_items via GET /api/.../questions and confirm your body keys match.
  3. 400 ValidationError with field: "responses" — the body did not include a top-level responses object. Wrap your keys inside {"responses": {...}}.
  4. 400 ValidationError with field: "depth"?depth= value is not basic, contextual, or full.
  5. 400 DeIdentificationViolation — strip the offending field or value and retry. See conventions.md.
  6. 404 NotFound — the (category, resource) pair is not registered. Cross-reference GET /api/catalog/resources.
  7. Score looks wrong for the inputs you sent. Read the per-instrument doc for the instrument's bands and scoring behavior, then compute the expected score by hand against the rubric and compare with finding.score and finding.band.
  8. Confidence tier is unexpectedly low. Read breakdown — it tells you which term is dragging the overall down. Small reference cohorts derate imputation_variance; a noisy phenotype classification derates model_certainty (at depth=full).

Rate limits

See rate-limits.md.

  • patterns-questions.md — the paired /questions operation, which describes the item IDs and answer choices that must feed this endpoint's body.
  • composite-cdri-compute.md — the cross-instrument, permissive-input counterpart.
  • Per-instrument docs (instruments-<resource>.md) — each one documents the specific item set, scoring method, band cutoffs, and clinical caveats for that instrument.
  • errors.md — full error taxonomy.

Change history

DateChange
2026-04-20Initial publication.
2026-06-03Bumped catalog_version to the live value; removed the tpa audience.