Loading documentation

Access required

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

Sign out
Skip to main content

Full Grade Composite

Status: beta Category: grading Added: 2026-06-22 Last reviewed: 2026-07-01

Summary

Grades a whole care team for a single case in one call and returns one role-filtered result per member. Each member receives a 0.01.0 grade, decomposed into priorities and the KPIs beneath them, with a human-readable rationale on every KPI. You send a de-identified bundle of case signals (timestamps and coordination events) plus an optional cross-priority priorities lever; the engine returns the graded breakdown.

POST /api/grading/case/composite grades across all seven priorities at once. To grade a single priority, call the per-priority variant documented in grading-case-priority.md — same request and response shape, scoped to one priority.

⚠ Not a clinical or performance verdict

A case grade is a decision-support signal about care-delivery process, not a clinical judgment, a performance review of any individual, or a benefits determination. The calling organization is responsible for how grades are interpreted and used. The engine resolves no identity and stores no application data.

Endpoint

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

Purpose

This endpoint answers "how well did the care team handle this case, broken down by who was responsible for what." It is the grading counterpart to the clinical-risk surfaces: where /cdri/compute scores a subject's risk, case grading scores the care team's handling of a case.

Call /grading/case/composite when you want the full grade across every priority. Call a single-priority variant when you only need one priority's view (for example, a timeliness-only dashboard tile).

Authentication

Every request must include a valid API key in the apikey header. Keys are issued per consumer; see authentication. A missing or unknown key returns 401 Unauthenticated at the gateway before the request reaches the engine.

Request

Headers

HeaderRequiredDescription
apikeyYesConsumer API key.
Content-TypeYesMust be application/json.
X-Request-IDNoOptional client-generated correlation ID (up to 128 chars). If omitted, the gateway generates one. The correlation ID is a header, not a body field — see Mistaken use.

Path parameters

None.

Query parameters

None.

Body

{
"case_id": "opaque-case-token",
"care_team": [
{ "clinician_user_id_hash": "c3ab8ff13720e8ad9047dd39466b3c89...", "role": "frontline" }
],
"context": { "phenotype": "msk_low_back", "severity": "moderate", "jurisdiction": "US-CA" },
"signals": {
"timeline": [
{ "kind": "case_opened", "actor_hash": null, "at": "2026-06-01T00:00:00Z", "ref": null },
{ "kind": "first_contact", "actor_hash": "c3ab8ff13720e8ad9047dd39466b3c89...", "at": "2026-06-01T02:00:00Z", "ref": "fc1" }
]
},
"priorities": { "timeliness": 100 }
}

Body fields

FieldTypeRequiredDescription
case_idstringYesAn opaque, de-identified token for the case. The engine echoes nothing derived from it and resolves no identity.
care_teamarrayYesThe members to grade. One entry per member; see care_team[]. An empty array returns an empty members array.
contextobject | nullNoCoarse, non-identifying case descriptors. See context.
signalsobjectNoThe de-identified signal bundle the grade is computed from. See Signals. Absent or {} yields a fully structured, unscored result.
prioritiesobject | nullNoThe cross-priority lever — {priority_key: number}. See The priorities lever. Absent, empty, or all-zero defaults to equal weight.

Unknown top-level fields are ignored.

care_team[] entries

FieldTypeRequiredDescription
clinician_user_id_hashstringYesA SHA-256 hash of your own user identifier for this member. You compute it; the engine never receives the raw identifier and only echoes the hash back. The same hash is used as actor_hash inside signals to attribute events to this member.
rolestringYesThe member's role archetype — one of ptp, case_mgr, frontline, navigator. See Role archetypes.

Role archetypes

Map each care-team member onto exactly one of four archetypes. Which KPIs a member is graded on follows from their archetype — a member is only graded on the KPIs their role is responsible for.

ValueTypically maps to
ptpThe primary treating provider.
case_mgrThe case manager.
frontlineFrontline clinical staff (for example a nurse or therapist).
navigatorThe care navigator / coordinator.

context

All fields optional and non-identifying. Provided for coarse case framing; send only what you have.

FieldTypeDescription
phenotypestring | nullA coarse case descriptor token.
severitystring | nullA coarse severity descriptor token.
jurisdictionstring | nullA coarse jurisdiction descriptor (for example a state/region token), never a street address.

De-identification requirement

All request bodies are scanned for identifiers on the way in. Do not include names, dates of birth, SSN-shaped values, email addresses, phone numbers, medical record numbers, or any free-text field that could carry PHI. Identify care-team members only by the SHA-256 clinician_user_id_hash you compute on your side. A body containing an identifier returns 400 DeIdentificationViolation naming the offending field. See conventions.

Signals

signals is a JSON object of named groups; each group is an array of event objects. Supply the groups you have — the engine grades the KPIs it can derive from the events you send and ignores event kinds it has no use for. The more relevant signal you supply, the more KPIs become covered. Sending no signals is valid: you get the full result structure with every KPI uncovered.

Events share a small common shape:

FieldTypeDescription
kindstringThe event type within its group (see each group below).
actor_hashstring | nullThe clinician_user_id_hash of the member who performed the event, or null for case-level anchors (for example case_opened). A member is graded only on events stamped with their own hash.
atstringISO 8601 timestamp of the event. A malformed or missing timestamp simply means the event does not contribute.
refstring | nullA correlation token used to pair an opening event with its closing event (for example an issue_opened with its issue_resolved). Opaque to the engine.

Some events carry a few extra fields (for example completeness counts on a document); those are listed with their group.

Signal groups

GroupWhat it informsNotable event kinds / fields
timelineTimeliness and case-handling KPIscase_opened, info_available (case-level anchors); first_contact, decision_made, intervention_started, employer_update_sent (member actions); encounter_ended + note_signed (paired by ref); handoff + note_signed; issue_opened + issue_resolved; external_wait_start + external_wait_end (see External waits)
coordinationCase-handling coordination KPIstask_assigned / thread_opened paired with task_closed / thread_closed; question_asked paired with question_answered (paired by ref)
referralsReferral hand-off KPIevent fields actor_hash, required_fields_present, required_fields_total (or boolean packet_complete)
documentationDocumentation-completeness KPIevent fields actor_hash, required_fields_present, required_fields_total
regulatoryRegulatory-completeness KPIevent fields actor_hash, required (boolean), filed (boolean), due_at, filed_at
ordersResource-use KPIevent fields actor_hash, at, order_code, new_indication (boolean)

External waits

Durations the engine reports are active durations: time a case spent waiting on something outside the care team's control is excluded. To declare such a window, send a paired external_wait_start and external_wait_end (matched by ref) in timeline. Any measured duration that overlaps the window has that overlap subtracted. Use this so a member is not charged for time spent waiting on an authorization, a specialist, or the patient.

Open loops

When an opening event has no matching closing event (an issue_opened with no issue_resolved, an encounter_ended with no note_signed), the affected KPI is reported as not yet resolvable rather than as a low score — see not_yet_resolvable in Response fields. Send the closing event later and call again to get the resolved grade.

The priorities lever

priorities is the only weighting input you control. It is a map of {priority_key: number} that governs how a member's per-priority scores combine into their overall score. Values are relative weights, not percentages — only their ratios matter. Keys you omit are treated as weight zero. An absent, empty, or all-zero lever defaults to equal weight across all priorities.

The seven priority keys are:

clinical_effectiveness, care_quality, timeliness, patient_experience, employer_experience, resource_utilization, case_handling.

The weighting of individual KPIs within a priority is not a caller input; it is fixed by the member's role archetype.

Response

Success — 200 OK

One result per care-team member. The example below is a real production capture for a single frontline member graded with full timeline signals; the priorities array is trimmed to the timeliness entry for readability — a real composite response also includes the other priorities this role is graded on (care_quality, clinical_effectiveness, patient_experience, resource_utilization, case_handling), each with the same shape.

{
"members": [
{
"clinician_user_id_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"role": "frontline",
"score": 1,
"coverage": 0.305,
"not_scored_reason": null,
"priorities": [
{
"key": "timeliness",
"score": 1,
"coverage": 1,
"not_scored_reason": null,
"kpis": [
{ "key": "decision_latency", "score": 1, "covered": true, "role_weight": 0.6, "rationale": "Decision in 11.0h active after info available: 1.00." },
{ "key": "documentation_timeliness", "score": 1, "covered": true, "role_weight": 0.58, "rationale": "1 note(s) signed promptly: 1.00." },
{ "key": "intervention_timing", "score": 1, "covered": true, "role_weight": 0.58, "rationale": "Intervention 10.0h active after decision: 1.00." },
{ "key": "responsiveness_consistency", "score": 1, "covered": true, "role_weight": 0.64, "rationale": "4 response measures, spread 0.00: 1.00." },
{ "key": "time_to_first_response", "score": 1, "covered": true, "role_weight": 1, "rationale": "First response in 2.0h active (target 4.0h): 1.00." }
]
}
]
}
],
"matrix_version": "2026-06-22-first-pass",
"catalog_version": "0.14.0",
"request_id": "docs-grading-case-composite",
"timestamp": "2026-06-22T22:20:30.793000+00:00"
}

Response fields

FieldTypeAlways presentDescription
membersarrayYesOne result per care-team member, in the order supplied. Empty when care_team was empty.
members[].clinician_user_id_hashstringYesEchoes the member's hash from the request.
members[].rolestringYesThe member's role archetype.
members[].scorenumber | nullYesThe member's overall grade, 0.01.0, as the lever-weighted roll-up of their scored priorities. null when nothing scorable (not_scored_reason says why).
members[].coveragenumberYesFraction 0.01.0 of this member's gradable surface that had supporting signal.
members[].not_scored_reasonstring | nullYesnull when score is present; otherwise insufficient_signal or not_yet_resolvable. See Not-scored reasons.
members[].prioritiesarrayYesThe priorities this member's role is graded on. Priorities the role does not own are omitted.
priorities[].keystringYesOne of the seven priority keys.
priorities[].scorenumber | nullYesThe priority grade, 0.01.0. null when coverage is too low to assert a grade (not_scored_reason says why).
priorities[].coveragenumberYesFraction 0.01.0 of this priority's KPIs (for this role) that were covered.
priorities[].not_scored_reasonstring | nullYesnull when score is present; otherwise insufficient_signal or not_yet_resolvable.
priorities[].kpisarrayYesThe KPIs that make up this priority for this role.
kpis[].keystringYesStable KPI identifier (for example time_to_first_response).
kpis[].scorenumber | nullYesThe KPI value, 0.01.0, or null when uncovered.
kpis[].coveredbooleanYestrue when the KPI had supporting signal and produced a score.
kpis[].role_weightnumberYesThe relative weight this KPI carries in this member's role-based grade. Diagnostic — display it, do not re-derive scores from it.
kpis[].rationalestringYesA short, identifier-free, human-readable explanation of the KPI's score, or of why it is uncovered.
matrix_versionstring | nullYesOpaque version tag for the active grading configuration. Do not pin to it.
catalog_versionstringYesCatalog version served.
request_idstringYesCorrelation ID.
timestampstring (ISO 8601 UTC)YesServer time at response emission.

Not-scored reasons

ReasonMeaningWhat to do
insufficient_signalToo little supporting signal was present to assert a grade at this level.Supply more of the relevant signal groups and call again.
not_yet_resolvableSignal exists but an outcome has not matured — an open loop with no closing event.Send the closing event when it happens, then call again. Do not read this as a low grade.

Coverage and partial results

Sparse results are normal and correct, not an error. A null score with a coverage below 1.0 means the engine did not have enough signal to grade that level — it never substitutes a zero for missing signal. As you supply more signal groups, coverage and the number of covered KPIs rise.

Coverage available today is partial: timeliness, case_handling, care_quality, and resource_utilization produce scores from the current signal set, and employer_experience covers its update-timeliness KPI. clinical_effectiveness and patient_experience are part of the grading framework but are not computed from the current signal set and return null. This surface is beta; coverage expands over time with no change to the request or response shape.

Status codes

CodeMeaningWhen
200OKRequest succeeded (including fully or partially unscored results).
400ValidationErrorMalformed JSON or an invalid field value. field names the offender when known.
400DeIdentificationViolationRequest body contained an identifier. field names the offending path.
401UnauthenticatedMissing or invalid API key.
404NotFoundPath does not exist or method is wrong.
413PayloadTooLargeBody exceeds the 512 KB limit.
415UnsupportedMediaTypeContent-Type is not application/json.
429RateLimitedPer-consumer rate limit exceeded. See Retry-After.
500InternalErrorUnexpected server-side failure. Include request_id when reporting.
503ServiceUnavailableA required dependency is temporarily unhealthy.

All non-2xx responses use the uniform error envelope; see errors.

Examples

curl

curl -sS -X POST "https://engine.tripod-health.com/api/grading/case/composite" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-grading-case-composite" \
-d '{
"case_id": "opaque-case-token",
"care_team": [
{ "clinician_user_id_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "role": "frontline" }
],
"signals": {
"timeline": [
{ "kind": "case_opened", "actor_hash": null, "at": "2026-06-01T00:00:00Z", "ref": null },
{ "kind": "first_contact", "actor_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "at": "2026-06-01T02:00:00Z", "ref": "fc1" }
]
},
"priorities": { "timeliness": 100 }
}'

Python (requests)

# Uses the call() helper from quickstart.md.
body = {
"case_id": "opaque-case-token",
"care_team": [
{"clinician_user_id_hash": "c" * 64, "role": "frontline"},
],
"signals": {
"timeline": [
{"kind": "case_opened", "actor_hash": None, "at": "2026-06-01T00:00:00Z", "ref": None},
{"kind": "first_contact", "actor_hash": "c" * 64, "at": "2026-06-01T02:00:00Z", "ref": "fc1"},
],
},
"priorities": {"timeliness": 100},
}
result = call("POST", "/api/grading/case/composite", body)
for member in result["members"]:
print(f"{member['role']}: {member['score']} (coverage {member['coverage']})")

Node (fetch)

const resp = await fetch(
"https://engine.tripod-health.com/api/grading/case/composite",
{
method: "POST",
headers: {
apikey: process.env.ENGINE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
case_id: "opaque-case-token",
care_team: [{ clinician_user_id_hash: "c".repeat(64), role: "frontline" }],
signals: {
timeline: [
{ kind: "case_opened", actor_hash: null, at: "2026-06-01T00:00:00Z", ref: null },
{ kind: "first_contact", actor_hash: "c".repeat(64), at: "2026-06-01T02:00:00Z", ref: "fc1" },
],
},
priorities: { timeliness: 100 },
}),
},
);
if (!resp.ok) throw new Error(`Engine ${resp.status}: ${await resp.text()}`);
const data = await resp.json();

Intended use

  • Grade a closed or in-progress case across every priority in one call.
  • Build a care-team scorecard: read each member's overall score plus the per-priority and per-KPI breakdown, and surface each KPI rationale to the user.
  • Re-call as a case progresses. Coverage rises as you send more signal, and not_yet_resolvable KPIs resolve once their closing events arrive.
  • Weight the roll-up to your program's priorities with the priorities lever without changing what signal you send.

Mistaken use

  • Do not put the correlation ID in the body. There is no request_id body field. Send the correlation ID in the X-Request-ID header; a request_id key in the body is ignored, not honored.
  • Do not send raw clinician or patient identifiers. Identify members only by the SHA-256 clinician_user_id_hash; the engine resolves no identity. Raw identifiers trip the de-identification filter (400).
  • Do not read a null score as a failing grade. null means not enough signal (insufficient_signal) or an open loop (not_yet_resolvable) — never a zero. Branch on not_scored_reason.
  • Do not use this to score a subject's clinical risk. That is /cdri/compute; case grading scores the care team's handling, not the patient's risk.
  • Need only one priority? Call the single-priority variant instead of filtering the composite client-side.

Debugging

  1. Capture the request_id from the response body or the X-Request-ID response header. Every support ticket starts here.
  2. A mostly-null result is usually correct. Check coverage and not_scored_reason at the member, priority, and KPI levels before assuming a bug — most nulls mean "no signal supplied for this KPI."
  3. KPI not scoring as expected? Confirm the relevant events carry the right member actor_hash and a valid ISO 8601 at, and that paired events share a ref (for example issue_opened/issue_resolved).
  4. 400 DeIdentificationViolation — the field extra names the offending path. Strip the identifier client-side before retrying.
  5. 429 RateLimited — respect the Retry-After response header; back off.
  6. 5xx — file a ticket with the request_id, the UTC timestamp, and a minimal reproducer body.

Rate limits

Per-consumer limits are enforced at the gateway. Specific numbers depend on your plan; see the rate limits reference. Responses include standard RateLimit-* headers.

Change history

DateChange
2026-06-22Initial publication (beta).
2026-07-01Renamed path from /api/case_outcome/composite to /api/grading/case/composite. Contract unchanged.