Encounter Document Draft
Status:
stableCategory:trinityassistAdded:2026-07-16Last reviewed:2026-09-28
Summary
Reads one finished visit's transcript and drafts its clinical note. What comes back depends on the visit's purpose:
- A full visit (
comprehensiveorstandard) gets the structured fields of a clinical note: the patient's reported history, the objective exam findings, the ranked differential, the plan, any vitals spoken aloud, and the work-status and work-relatedness proposals. Every value is a code from an exam catalog you supply the version of, and the response carries no prose -- you compose your own narrative from the structure. - A check-in (
check_in) gets a short narrative note instead: a few sentences of the medically relevant content of the call, written in the clinician's voice. A check-in has no coded document to fill, so prose is the whole result.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/encounters/document/draft |
| Full URL | https://engine.tripod-health.com/api/encounters/document/draft |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No (each call may return a slightly different draft) |
| Rate-limited | Yes |
Purpose
The endpoint exists so you can assemble a pre-filled clinician sign-off surface from a visit transcript in one call.
For a full visit it does not document the visit. It has no opinion about what the note should say. It reads the transcript and ticks the boxes the transcript supports, leaving everything else blank. The clinician reviews the selections, adjusts them, and composes the note. (A check-in is the exception -- see Check-ins return a narrative note.)
That framing drives every design decision below, and it is worth internalizing before you integrate:
- A blank field is a correct answer. A field is filled only when the transcript supports it. There is no inference to a plausible default and no reasoning from what a typical visit contains.
- Precision is the goal, not coverage. A draft with six correct selections and forty blanks is a good result. A draft with forty selections of which six are invented is worse than an empty one, because the clinician's job silently changes from writing a note to auditing a machine.
- Expect most fields to be null, particularly vitals on a remote visit.
Why a full visit gets no prose
You compose the narrative from the codes this endpoint returns, using whatever composer you already use for a hand-written note. That means one composer produces every note in your system, so a machine-drafted note and a hand-written one are indistinguishable in format and cannot drift apart. If the engine also generated prose you would have two composers, and a clinician can tell which sentences came from a machine because they read differently.
The four narrative fields (soap.subjective, soap.objective,
soap.assessment, soap.plan) are present in the response and are always
null.
Check-ins return a narrative note
A check-in is a short touch base, and its record is a free-text note rather than
a structured exam, so there is nothing coded to pre-fill. Send
encounter_purpose: "check_in" and the response is that note, drafted from the
call:
- Medically relevant content only. How the patient reports they are doing and any change since they were last seen; activity, work, and adherence to the current plan; new concerns; and any detail about the patient that was confirmed, added, corrected, or denied. Small talk is left out unless it bears on the condition -- a long walk on holiday matters to an ankle sprain; the destination does not.
- Written as the clinician's own note. Third person, past tense, professional register. It never addresses the clinician and never mentions how it was documented.
- Never empty. A call with nothing clinical in it still returns a note saying so, with the patient's general demeanor where the conversation shows it -- so an empty note never has to be read as "did the draft run?".
- Short. A few sentences, with no headings or lists. It is not padded.
Send encounter_meta.clinician_role so the note is written for the right
reader, and encounter_meta.call_duration_s to have the note end with the
visit's length, stated exactly as Visit length 2m 23s.
Authentication
Standard API key. Set apikey: $ENGINE_API_KEY on the request. See
authentication.md. A missing or unknown key returns
401 Unauthenticated at the gateway before the request reaches the engine.
Request
Headers
| Header | Required | Notes |
|---|---|---|
apikey | Yes | Your engine API key. |
Content-Type | Yes | application/json. |
Body
{
"sessions": [
{
"session_id": "00000000-0000-0000-0000-000000000001",
"started_at": "2026-08-05T14:02:00.000Z",
"segments": [
{
"speaker": "provider",
"speaker_label": "Provider",
"text": "Tell me what's been going on with your back.",
"started_at_ms": 0,
"ended_at_ms": 3500
},
{
"speaker": "patient",
"speaker_label": "Patient",
"text": "It's been about a week. It came on right after I lifted a stack of boxes at work.",
"started_at_ms": 3500,
"ended_at_ms": 12000
}
]
}
],
"encounter_meta": {
"encounter_purpose": "comprehensive",
"visit_type": "intake",
"encounter_medium": "telemedicine",
"visit_kind": null
},
"catalog_version": "1"
}
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
sessions | array | Yes | The complete transcript, one entry per recording session. At least one segment total is required. |
sessions[].session_id | string | Yes | Your identifier for the session. Opaque to the engine. |
sessions[].started_at | string | Yes | ISO date-time the session began. |
sessions[].segments[].speaker | string | Yes | provider, patient, other, or unknown. |
sessions[].segments[].speaker_label | string | null | No | Display label for the speaker. |
sessions[].segments[].text | string | Yes | The utterance. Max 8,000 characters. |
sessions[].segments[].started_at_ms | integer | Yes | Offset from the session start. |
sessions[].segments[].ended_at_ms | integer | Yes | Offset from the session start. |
encounter_meta.encounter_purpose | string | Yes | comprehensive or standard for a full visit (coded sections back); check_in for a check-in (a narrative note back). Any other value returns 400 ValidationError. |
encounter_meta.visit_type | string | Yes | intake or follow_up. |
encounter_meta.encounter_medium | string | Yes | telemedicine, telephonic, or messaging. |
encounter_meta.visit_kind | string | null | Yes | The visit kind, when a clinician has already chosen one. See below. Ignored for a check-in; send null. |
encounter_meta.clinician_role | string | null | No | Check-in only. The clinician's discipline as a plain professional title, e.g. Physician. Max 64 characters. A title, never a name. Omitted or null writes the note for "the clinician". Ignored for a full visit. |
encounter_meta.call_duration_s | integer | null | No | Check-in only. The visit's length in whole seconds, 0 to 86400. When sent, the note ends with it, e.g. Visit length 2m 23s. Ignored for a full visit. |
catalog_version | string | Yes | The exam catalog version the response must be coded against. Required for a check-in too, though a check-in's note carries no codes and does not check it. |
visit_kind -- send null to have it inferred
The visit kind is the root of the exam taxonomy: it selects which objective exam items, which differential categories, and which plan interventions apply.
- Send
nullwhen the clinician has not chosen one yet. The engine infers it from the transcript and returns it atsoap.visitKindso you can preselect it. - Send a kind when the clinician has already chosen one. It is then authoritative and the engine will not override it, even if the transcript suggests otherwise. The clinician was in the room.
If the transcript does not establish a kind, the engine returns
soap.visitKind: null and an empty exam. A finding recorded under no visit
kind cannot be resolved against any catalog, so returning one would be worse than
returning nothing.
catalog_version -- pin the vocabulary
Every string in the response is a code from your exam catalog. catalog_version
names which version of it to code against, and the engine echoes that value at
soap.exam.catalogVersion. Store it alongside the selections: a code is only
meaningful against the catalog that produced it, so a stored selection needs the
version to be read back correctly later.
If the engine has no catalog for the version you ask for it returns
400 ValidationError naming the field, rather than guessing against a different
vocabulary. Coordinate catalog changes through deploy sequencing.
Beware: visit_type is not the visit's purpose
Two distinct axes, and they are easy to conflate:
| Field | Answers | Values |
|---|---|---|
encounter_purpose | How substantial is this contact? | comprehensive, standard, check_in |
visit_type | Is this the first visit or a later one? | intake, follow_up |
An earlier version of this endpoint used visit_type to hold the purpose. It
does not any more.
Response
The shape depends on encounter_purpose. A full visit returns the five coded
sections below; a check-in returns one note section -- see
Check-in response.
For a full visit, 200 OK returns five top-level sections. Every field of every
section is always present; "not established by the transcript" is expressed as
null, [], or {}, never as a missing key.
| Section | What it is |
|---|---|
soap | The visit kind, the four always-null narratives, and the structured exam. |
vitals | Six point-of-assessment vitals. All null unless numbers were spoken aloud. |
workRelatedness | The case-level work-relatedness inputs. Never a verdict. |
workStatus | The carrier-facing work-status proposal. |
disposition | Always empty. See below. |
{
"soap": {
"visitKind": "musculoskeletal",
"subjective": null,
"objective": null,
"assessment": null,
"plan": null,
"exam": {
"catalogVersion": "1",
"subjective": {
"chiefComplaint": "It came on right after I lifted a stack of boxes at work.",
"onsetWhen": "1_4_weeks",
"onsetMode": "after_activity",
"episodeDuration": "most_of_day",
"quality": ["dull_aching", "sharp"],
"timing": ["worse_with_activity"],
"aggravating": ["lifting", "bending"],
"relieving": ["rest", "ice"],
"radiates": false,
"radiatesTo": null,
"severityCurrent": 4,
"severityBest": 2,
"severityWorst": 6,
"ros": { "musculoskeletal": ["back_pain"] },
"rosDenies": ["neuro"],
"adls": ["lifting_carrying", "bending_stooping"],
"overallLimitation": "mildly_limited",
"reportedWorkStatus": "modified_duty",
"mainConcern": "not being able to work",
"causeBelief": "work_task_or_event",
"worries": ["cant_work"],
"visitGoals": ["pain_relief", "return_to_work"],
"lifeEffect": "Lifting and bending, mostly."
},
"objective": {
"areas": ["lumbar"],
"findings": [
{
"areaKey": "lumbar",
"subsectionKey": "arom",
"itemKey": "ar_0",
"result": "choice",
"choiceValue": "limited"
}
]
},
"assessment": {
"differentials": [
{
"dxKey": "lumbar_strain",
"label": "Lumbar strain / sprain",
"categoryKey": "msk_spine",
"likelihood": "most_likely"
}
],
"acuity": "acute",
"trajectory": null,
"severity": "mild",
"laterality": "midline",
"workRelationship": "industrial",
"causationCertainty": "probable",
"apportionment": null,
"mmiStatus": null,
"redFlags": []
},
"plan": {
"picks": {
"act_mods": ["relative_rest", "avoid_aggravating"],
"rice_components": ["ice"]
},
"params": { "edu_followup": "1_week" }
}
},
"structuredExtraction": {}
},
"vitals": {
"systolicBp": null,
"diastolicBp": null,
"heartRate": null,
"respiratoryRate": null,
"temperatureF": null,
"spo2": null
},
"workRelatedness": {
"initialAssessment": null,
"patientClaim": null,
"employerClaim": null
},
"workStatus": {
"status": null,
"effectiveFrom": null,
"effectiveThrough": null,
"restrictions": null
},
"disposition": {
"decision": null,
"followupAt": null,
"followupChannel": null,
"closeReason": null
}
}
Response fields
soap.exam.subjective
The patient's reported story. Coded fields draw from your subjective catalog.
Four fields are the patient's own words and carry no vocabulary:
chiefComplaint, mainConcern, lifeEffect, and radiatesTo. They are quoted
from the transcript rather than translated into clinical register, because the
patient's phrasing is itself clinically useful.
severityCurrent, severityBest, and severityWorst are whole numbers 0-10,
populated only when the patient actually gave a number.
ros maps a system key to the symptoms the patient affirmed. rosDenies
lists systems the patient denied wholesale. A system in neither was never asked
about -- "not asked" and "asked and negative" are different facts, so they never
collapse into one. A system never appears in both.
radiatesTo is only populated when radiates is true.
soap.exam.objective
areas lists the body areas evaluated, and is populated only for
musculoskeletal visits -- it is the one visit kind with a body-area layer.
Every other kind returns [] and every finding carries areaKey: null.
areas contains only areas that carry at least one finding. The engine cannot
tell you "I examined the knee and found nothing remarkable" -- that is a
clinician's positive claim, not something readable from silence.
Each finding is identified by the full tuple (visitKind, areaKey, subsectionKey, itemKey). Item keys are not globally unique: ar_0 is one
item for the lumbar spine and a different item for the jaw. Never resolve an
itemKey without its area and subsection.
result is always "choice", and choiceValue is always non-null. The engine
never returns not_tested, which means a clinician considered this item and
deliberately did not perform it -- a claim about what happened in the room that
only a clinician can make. An item that never came up is simply absent from the
array.
soap.exam.assessment
differentials is the ranked working differential.
- When the diagnosis is in your catalog,
dxKeyis its code andlabelis the catalog's own wording. - When it genuinely is not,
dxKeyisnullandlabelcarries the typed name. Those free-text entries are a useful signal for what to add to your catalog next. categoryKeyis derived fromdxKeyand isnullfor a free-text entry.
The characterization and causation fields are single codes or null.
workRelationship, causationCertainty, apportionment, and mmiStatus are
the causation opinion on this encounter -- clinical reasoning, distinct from
the case-level workRelatedness section below.
redFlags is normally an empty array.
soap.structuredExtraction
Always {}. It is a legacy advisory field superseded by exam; two
independent reads of the same note would only ever disagree.
vitals
Six fields, all optional, all typically null. On a remote visit a vital is
only available if the patient read it off a home device out loud. The engine
never estimates one.
Height, weight, BMI, and a pain score are deliberately absent: height and weight belong with chart demographics, and pain is captured far more richly as the subjective severity triple.
workStatus and workRelatedness
Clinician attestations. The engine pre-fills one only when the transcript records
the clinician actually stating it, so null is the common and correct case.
workStatus.status is full_duty, modified_duty, or off_work. There is no
MMI value here -- MMI is soap.exam.assessment.mmiStatus, a different axis.
modified_duty is only ever returned with restrictions attached; if the engine
cannot name the restrictions it returns status: null instead.
workRelatedness returns inputs, never a verdict. Recompute the
determination yourself. If initialAssessment is unsure, patientClaim is
always populated alongside it.
disposition
Always empty. Whether to schedule a follow-up or close a case is a decision about the case rather than a reading of one transcript, and a follow-up needs a time and channel the engine has no basis to choose. The section is present so the response shape stays stable.
Unknown codes are dropped, not returned
Every code is validated against the catalog before the response is sent. Anything that does not resolve is dropped from the response -- you never receive an invalid code, and the endpoint does not fail because one selection was unresolvable.
That is deliberate. A thin draft is useful; a failed call is not, especially if your workflow allows one drafting pass per encounter. If a response is emptier than you expected, the transcript is the usual reason.
Check-in response
200 OK for encounter_purpose: "check_in" returns a single section:
{
"note": {
"note": "Check-in call completed. Patient reports lower back pain improving since last visit, now 3 out of 10, and is tolerating modified duty. Doing the home exercises most days; no new symptoms. Visit length 4m 12s."
}
}
A call with nothing clinical in it still returns a note:
{
"note": {
"note": "Check-in call completed. No clinically relevant concerns were discussed. Patient's demeanor was positive. Visit length 2m 23s."
}
}
| Field | Type | Always present | Description |
|---|---|---|---|
note | object | Yes | The check-in's note section. |
note.note | string | Yes | The drafted narrative: a few sentences in the clinician's voice, ending with the visit length when call_duration_s was sent. Always a non-empty string. |
None of the full-visit sections (soap, vitals, workRelatedness,
workStatus, disposition) appear on a check-in response. Work status and
disposition stay the clinician's to set.
Status codes
| Status | Error | Meaning |
|---|---|---|
200 | -- | A draft was produced. It may be sparse. |
400 | ValidationError | Malformed body, no transcript segments, or an unsupported catalog_version. The message names the field. |
401 | Unauthenticated | Missing or unknown API key. |
402 | PaymentRequired | Out of credits. |
429 | RateLimitExceeded | Per-consumer rate limit. |
502 | UpstreamError | Generation failed, or generated free text failed the outbound de-identification check. |
504 | UpstreamTimeout | Generation exceeded the engine's budget. |
Examples
curl
curl -sS -X POST https://engine.tripod-health.com/api/encounters/document/draft \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessions": [
{
"session_id": "00000000-0000-0000-0000-000000000001",
"started_at": "2026-08-05T14:02:00.000Z",
"segments": [
{
"speaker": "patient",
"speaker_label": "Patient",
"text": "My lower back has hurt for about a week, since I lifted some boxes at work.",
"started_at_ms": 0,
"ended_at_ms": 8000
}
]
}
],
"encounter_meta": {
"encounter_purpose": "comprehensive",
"visit_type": "intake",
"encounter_medium": "telemedicine",
"visit_kind": null
},
"catalog_version": "1"
}'
Python
import requests
resp = requests.post(
"https://engine.tripod-health.com/api/encounters/document/draft",
headers={"apikey": ENGINE_API_KEY},
json={
"sessions": sessions,
"encounter_meta": {
"encounter_purpose": "comprehensive",
"visit_type": "intake",
"encounter_medium": "telemedicine",
# None asks the engine to infer it; a chosen kind is authoritative.
"visit_kind": None,
},
"catalog_version": EXAM_CATALOG_VERSION,
},
timeout=60,
)
resp.raise_for_status()
document = resp.json()
exam = document["soap"]["exam"]
print("visit kind:", document["soap"]["visitKind"])
print("catalog:", exam["catalogVersion"])
for finding in exam["objective"]["findings"]:
print(finding["areaKey"], finding["subsectionKey"], finding["itemKey"], "=", finding["choiceValue"])
for dx in exam["assessment"]["differentials"]:
print(dx["likelihood"], dx["dxKey"] or "(free text)", dx["label"])
Python -- a check-in
resp = requests.post(
"https://engine.tripod-health.com/api/encounters/document/draft",
headers={"apikey": ENGINE_API_KEY},
json={
"sessions": sessions,
"encounter_meta": {
"encounter_purpose": "check_in",
"visit_type": "follow_up",
"encounter_medium": "telephonic",
"visit_kind": None,
# A plain professional title, never a name.
"clinician_role": "Physician",
# The visit's length in whole seconds; the note ends with it.
"call_duration_s": 143,
},
"catalog_version": EXAM_CATALOG_VERSION,
},
timeout=60,
)
resp.raise_for_status()
print(resp.json()["note"]["note"])
Intended use
- Call it once per encounter, after the visit has ended, with the complete transcript. There is no windowed or incremental mode.
- Render the returned selections as editable controls, let the clinician adjust them, compose your narrative from the final structure, and persist at sign-off.
- Store
soap.exam.catalogVersionwith the selections so they remain resolvable after your catalog changes. - Preselect
soap.visitKindwhen you sentnull, and keep your own value when you sent one. - For a check-in, place
note.notein the note field for the clinician to read, edit, and sign, and sendcall_duration_sfrom the visit's own start and end times rather than from the transcript offsets, which run from first word to last and undercount the call. - Budget a 60 second client timeout. The engine's own budget sits below that,
so a slow generation returns a clean
504rather than a client-side abort.
Mistaken use
- Persisting the document without clinician review. Every field is a draft or a proposal. Nothing here is a clinical record until a clinician signs.
- Expecting prose from a full visit. The four narrative fields are always
nullby design; compose the narrative from the returned structure instead. If the encounter is a short touch base whose record is a free-text note, send it ascheck_inand the note comes back written. - Sending a check-in to get coded fields. A check-in returns only its note.
If the visit needs a structured exam, send it as
comprehensiveorstandardinstead. - Putting a name in
clinician_role. It is a discipline, not a person; send the professional title (Physician), ornull. - Reading
workRelatednessas a decision. It is an input set; recompute the determination yourself. - Resolving an
itemKeywithout its area and subsection. Item keys are positional within a subsection, so the same key means different things in different body areas. - Treating a sparse response as a failure. Blank fields are the expected output for a transcript that did not cover them.
- Calling it on a timer, or more than once per encounter. It is built for a single explicit pass over a finished transcript. Repeatedly redrafting a note a clinician is editing makes "what is in my note and where did it come from" unanswerable.
Debugging
- Capture the
request_idfrom the error envelope before anything else. A successful draft has none in its body; capture theX-Request-IDresponse header (the gateway's trace ID, which support can also look up) instead. 400 ValidationError "At least one transcript segment is required"-- the request carried no segments across any session. Confirm your transcript actually persisted before calling.400 ValidationError "catalog_version: version X is not supported"-- the engine has no catalog for that version. Check your deploy sequencing.soap.visitKindis null and the exam is empty -- the transcript did not establish a visit kind. Send the clinician's chosen kind explicitly if they have picked one.- The response is valid but sparse -- the usual cause is a transcript that did not cover those fields. Verify the transcript is complete and that consecutive segments are distinct utterances rather than a repeated running aggregate.
502 UpstreamError "Generated text failed safety check: ..."-- generated free text carried an identifier shape and the engine refused to return it. On a check-in the whole note is free text, so this is the check-in's most likely failure. Retry; if it persists, open a ticket with therequest_id.- A check-in note does not state the visit length --
call_duration_swas omitted,null, or0. 504 UpstreamTimeout-- generation exceeded the engine's budget. Retry once.
Rate limits
Per-consumer limits are enforced at the gateway. See
rate-limits.md. Responses include standard RateLimit-*
headers.
Related
- conventions.md -- de-identification contract, request IDs, timestamp shape.
- errors.md -- error envelope and code list.
- credits.md -- per-call credit cost.
Change history
| Date | Change |
|---|---|
| 2026-09-28 | Added check-ins. encounter_purpose accepts check_in, which returns a short narrative note (note.note) instead of the coded sections, and is never empty. Added the optional encounter_meta.clinician_role and encounter_meta.call_duration_s, both check-in only. Additive: full-visit requests and responses are unchanged. |
| 2026-08-08 | Breaking rewrite. The response is now coded selections against a versioned exam catalog instead of narrative prose. catalog_version is required. encounter_meta renamed visit_type (which held the purpose) to encounter_purpose, added a real visit_type axis (intake / follow_up), renamed encounter_type to encounter_medium, and added visit_kind. Removed open_prior_goals and the injuryBaseline, patientBaseline, sinceLastVisit, and nextVisitGoals response sections. Narrowed vitals to six fields. |
| 2026-07-16 | Initial publication. |
| 2026-10-14 | The engine assigns every request ID; an X-Request-ID request header is not used. The X-Request-ID response header is the gateway's trace ID for the request and does not match request_id; quote request_id to support. |