Full Grade Composite
Status:
betaCategory:gradingAdded:2026-06-22Last 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.0–1.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
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/grading/case/composite |
| Full URL | https://engine.tripod-health.com/api/grading/case/composite |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No |
| Rate-limited | Yes |
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
| Header | Required | Description |
|---|---|---|
apikey | Yes | Consumer API key. |
Content-Type | Yes | Must be application/json. |
X-Request-ID | No | Optional 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
| Field | Type | Required | Description |
|---|---|---|---|
case_id | string | Yes | An opaque, de-identified token for the case. The engine echoes nothing derived from it and resolves no identity. |
care_team | array | Yes | The members to grade. One entry per member; see care_team[]. An empty array returns an empty members array. |
context | object | null | No | Coarse, non-identifying case descriptors. See context. |
signals | object | No | The de-identified signal bundle the grade is computed from. See Signals. Absent or {} yields a fully structured, unscored result. |
priorities | object | null | No | The 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
| Field | Type | Required | Description |
|---|---|---|---|
clinician_user_id_hash | string | Yes | A 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. |
role | string | Yes | The 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.
| Value | Typically maps to |
|---|---|
ptp | The primary treating provider. |
case_mgr | The case manager. |
frontline | Frontline clinical staff (for example a nurse or therapist). |
navigator | The care navigator / coordinator. |
context
All fields optional and non-identifying. Provided for coarse case framing; send only what you have.
| Field | Type | Description |
|---|---|---|
phenotype | string | null | A coarse case descriptor token. |
severity | string | null | A coarse severity descriptor token. |
jurisdiction | string | null | A 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:
| Field | Type | Description |
|---|---|---|
kind | string | The event type within its group (see each group below). |
actor_hash | string | null | The 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. |
at | string | ISO 8601 timestamp of the event. A malformed or missing timestamp simply means the event does not contribute. |
ref | string | null | A 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
| Group | What it informs | Notable event kinds / fields |
|---|---|---|
timeline | Timeliness and case-handling KPIs | case_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) |
coordination | Case-handling coordination KPIs | task_assigned / thread_opened paired with task_closed / thread_closed; question_asked paired with question_answered (paired by ref) |
referrals | Referral hand-off KPI | event fields actor_hash, required_fields_present, required_fields_total (or boolean packet_complete) |
documentation | Documentation-completeness KPI | event fields actor_hash, required_fields_present, required_fields_total |
regulatory | Regulatory-completeness KPI | event fields actor_hash, required (boolean), filed (boolean), due_at, filed_at |
orders | Resource-use KPI | event 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
| Field | Type | Always present | Description |
|---|---|---|---|
members | array | Yes | One result per care-team member, in the order supplied. Empty when care_team was empty. |
members[].clinician_user_id_hash | string | Yes | Echoes the member's hash from the request. |
members[].role | string | Yes | The member's role archetype. |
members[].score | number | null | Yes | The member's overall grade, 0.0–1.0, as the lever-weighted roll-up of their scored priorities. null when nothing scorable (not_scored_reason says why). |
members[].coverage | number | Yes | Fraction 0.0–1.0 of this member's gradable surface that had supporting signal. |
members[].not_scored_reason | string | null | Yes | null when score is present; otherwise insufficient_signal or not_yet_resolvable. See Not-scored reasons. |
members[].priorities | array | Yes | The priorities this member's role is graded on. Priorities the role does not own are omitted. |
priorities[].key | string | Yes | One of the seven priority keys. |
priorities[].score | number | null | Yes | The priority grade, 0.0–1.0. null when coverage is too low to assert a grade (not_scored_reason says why). |
priorities[].coverage | number | Yes | Fraction 0.0–1.0 of this priority's KPIs (for this role) that were covered. |
priorities[].not_scored_reason | string | null | Yes | null when score is present; otherwise insufficient_signal or not_yet_resolvable. |
priorities[].kpis | array | Yes | The KPIs that make up this priority for this role. |
kpis[].key | string | Yes | Stable KPI identifier (for example time_to_first_response). |
kpis[].score | number | null | Yes | The KPI value, 0.0–1.0, or null when uncovered. |
kpis[].covered | boolean | Yes | true when the KPI had supporting signal and produced a score. |
kpis[].role_weight | number | Yes | The relative weight this KPI carries in this member's role-based grade. Diagnostic — display it, do not re-derive scores from it. |
kpis[].rationale | string | Yes | A short, identifier-free, human-readable explanation of the KPI's score, or of why it is uncovered. |
matrix_version | string | null | Yes | Opaque version tag for the active grading configuration. Do not pin to it. |
catalog_version | string | Yes | Catalog version served. |
request_id | string | Yes | Correlation ID. |
timestamp | string (ISO 8601 UTC) | Yes | Server time at response emission. |
Not-scored reasons
| Reason | Meaning | What to do |
|---|---|---|
insufficient_signal | Too little supporting signal was present to assert a grade at this level. | Supply more of the relevant signal groups and call again. |
not_yet_resolvable | Signal 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
| Code | Meaning | When |
|---|---|---|
200 | OK | Request succeeded (including fully or partially unscored results). |
400 | ValidationError | Malformed JSON or an invalid field value. field names the offender when known. |
400 | DeIdentificationViolation | Request body contained an identifier. field names the offending path. |
401 | Unauthenticated | Missing or invalid API key. |
404 | NotFound | Path does not exist or method is wrong. |
413 | PayloadTooLarge | Body exceeds the 512 KB limit. |
415 | UnsupportedMediaType | Content-Type is not application/json. |
429 | RateLimited | Per-consumer rate limit exceeded. See Retry-After. |
500 | InternalError | Unexpected server-side failure. Include request_id when reporting. |
503 | ServiceUnavailable | A 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
scoreplus the per-priority and per-KPI breakdown, and surface each KPIrationaleto the user. - Re-call as a case progresses. Coverage rises as you send more signal, and
not_yet_resolvableKPIs resolve once their closing events arrive. - Weight the roll-up to your program's priorities with the
prioritieslever without changing what signal you send.
Mistaken use
- Do not put the correlation ID in the body. There is no
request_idbody field. Send the correlation ID in theX-Request-IDheader; arequest_idkey 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
nullscore as a failing grade.nullmeans not enough signal (insufficient_signal) or an open loop (not_yet_resolvable) — never a zero. Branch onnot_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
- Capture the
request_idfrom the response body or theX-Request-IDresponse header. Every support ticket starts here. - A mostly-
nullresult is usually correct. Checkcoverageandnot_scored_reasonat the member, priority, and KPI levels before assuming a bug — mostnulls mean "no signal supplied for this KPI." - KPI not scoring as expected? Confirm the relevant events carry the right
member
actor_hashand a valid ISO 8601at, and that paired events share aref(for exampleissue_opened/issue_resolved). 400 DeIdentificationViolation— thefieldextra names the offending path. Strip the identifier client-side before retrying.429 RateLimited— respect theRetry-Afterresponse header; back off.5xx— file a ticket with therequest_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.
Related
- grading-case-priority.md — grade a single priority; same shape, scoped.
- conventions.md — timestamps, request IDs, null-vs-missing, the de-identification boundary.
- errors.md — the error envelope and full
errortaxonomy. - composite-cdri-compute.md — score a subject's clinical risk (distinct from grading the care team).
Change history
| Date | Change |
|---|---|
| 2026-06-22 | Initial publication (beta). |
| 2026-07-01 | Renamed path from /api/case_outcome/composite to /api/grading/case/composite. Contract unchanged. |