Loading documentation

Access required

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

Sign out
Skip to main content

SOAP Draft

Status: stable Category: trinityassist Added: 2026-05-12 Last reviewed: 2026-05-26

Summary

Generate a SOAP-format clinical note draft from one clinician-patient encounter. Returns four narrative sections (Subjective / Objective / Assessment / Plan) plus a structured extraction (vitals, assessments, plan items, ICD-10 candidates, chief complaint, follow-up).

The endpoint supports three call modes that drive both the model tier and the prompt framing:

  • on_demand (default): the legacy "draft button" path. Ship the full diarized transcript as sessions and get a draft on the engine's default tier. Use this when a clinician asks for a draft once, after the visit.
  • refresh: periodic mid-visit refresh. Ship a pre-condensed trimmed_transcript produced by /api/encounters/transcript/trim; sessions may be empty. Runs on the Haiku tier so a 5-minute cadence stays cost-bounded.
  • final: end-of-call wrap-up. Same input shape as refresh but runs on Sonnet for the richest structured extraction.

This is a per-encounter TrinityAssist clinical surface. The consumer renders the returned narrative + extraction in editable review surfaces; the clinician edits and signs off on its side. At case close, the consumer can generate typed reflection questions via /api/case_reflection/questions and forward the signed answers via /api/case_reflection/ingest.

Endpoint

FieldValue
MethodPOST
Path/api/encounters/soap/draft
Full URLhttps://engine.tripod-health.com/api/encounters/soap/draft
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo (each call may return a slightly different draft)
Rate-limitedYes

Generated text passes the engine's outbound de-identification scan before returning; identifier-shaped output fails with 502 UpstreamError rather than scrubbing silently.

Authentication

Standard API-key. Set apikey: $ENGINE_API_KEY on the request. See authentication.md.

Request

Headers

HeaderRequiredNotes
apikeyYesYour engine API key.
Content-TypeYesapplication/json.
X-Request-IDNoEchoed back; lets you correlate logs.

Body

{
"sessions": [
{
"session_id": "00000000-0000-0000-0000-000000000001",
"started_at": "2026-05-12T14:02:00.000Z",
"segments": [
{
"speaker": "provider",
"speaker_label": "Provider",
"text": "Hello, can you describe what brings you in today?",
"started_at_ms": 0,
"ended_at_ms": 3500
},
{
"speaker": "patient",
"speaker_label": "Patient",
"text": "I've had lower-back pain for about a week...",
"started_at_ms": 3500,
"ended_at_ms": 14000
}
]
}
],
"encounter_meta": {
"visit_type": "initial",
"encounter_type": "in_person"
}
}
FieldTypeAlways presentDescription
sessionsarrayYesDiarized transcript sessions ordered chronologically by each session's first segment. Required field. May be an empty array when trimmed_transcript carries the content (refresh / final modes). A re-Started visit appears as session 2. Max 32 sessions.
sessions[].session_idstringYesConsumer-supplied opaque session UUID. Treated as opaque by the engine. Max 64 chars.
sessions[].started_atstringYesISO 8601 timestamp of the session's first segment.
sessions[].segmentsarrayYesNon-empty. Diarized transcript turns in chronological order. Max 4,096 segments per session.
sessions[].segments[].speakerenumYesOne of provider, patient, other, unknown.
sessions[].segments[].speaker_labelstring | nullNoOptional human-readable label. The engine treats it as opaque. Max 128 chars.
sessions[].segments[].textstringYesUtterance text. Max 8,000 chars per segment.
sessions[].segments[].started_at_msintegerYesMilliseconds since session start.
sessions[].segments[].ended_at_msintegerYesMilliseconds since session start.
encounter_metaobjectYesEncounter context for prompt shaping.
encounter_meta.visit_typeenumYesOne of initial, follow_up, touchbase, amendment, final.
encounter_meta.encounter_typeenumYesOne of in_person, telemedicine, telephonic.
call_modeenumNoOne of on_demand (default), refresh, or final. Drives the model tier the engine selects -- see Summary.
trimmed_transcriptstring | nullNoPre-condensed clinical-content extract produced by /api/encounters/transcript/trim. When present and non-empty, the model prefers it over the raw sessions array. Max 64,000 chars. Required when sessions is empty.

At least one of sessions (non-empty) or trimmed_transcript (non-empty) MUST be provided. The engine rejects a request with both empty as 400 ValidationError: "Either sessions or trimmed_transcript must be non-empty".

The body MUST NOT contain identifier-shaped values. The engine's inbound de-identification middleware rejects any such payload with 400 DeIdentificationViolation. See conventions.md.

Response

Success -- 200 OK

{
"subjective": "The patient presents with lower-back pain that began approximately one week ago after lifting boxes at work. Pain is worse in the morning and improves with movement. The patient rates pain intensity as six out of ten at baseline, with acute spikes to eight out of ten on forward bending. No associated numbness, tingling, or leg weakness reported; pain is localized to the lower back. The patient reports a similar episode approximately one year ago that resolved spontaneously within days without intervention.",
"objective": "Physical examination reveals mildly limited range of motion in lumbar flexion. Straight-leg raise test is negative bilaterally. No focal neurological deficits noted on lower-extremity examination. Vital signs stable: blood pressure 124/78 mmHg.",
"assessment": "Acute lower-back pain, likely mechanical in etiology, precipitated by work-related lifting. History of a prior similar episode one year ago suggests predisposition to recurrent strain injury. No evidence of radiculopathy or acute neurological compromise on current examination. Clinical presentation and exam findings are consistent with acute muscular strain or minor ligamentous injury without neurological involvement.",
"plan": "1. Initiate nonsteroidal anti-inflammatory medication (NSAID) for pain and inflammation management.\n2. Prescribe home mobility exercises and activity modification to promote recovery and prevent re-injury.\n3. Emphasize proper body mechanics with lifting and bending.\n4. Instruct patient to call immediately if pain worsens or if new leg symptoms (numbness, tingling, weakness) develop.\n5. Follow-up visit in two weeks to reassess response to conservative management; consider advanced imaging or specialist referral if symptoms do not substantially improve.",
"structured_extraction": {
"chief_complaint": "Lower-back pain",
"vitals": [
{ "name": "Blood Pressure", "value": "124/78", "unit": "mmHg", "ts": null }
],
"assessments": [
{ "summary": "Acute mechanical lower-back pain precipitated by occupational lifting; no radiculopathy on exam. Recurrent history (prior episode one year ago).", "confidence": null }
],
"plan_items": [
{ "summary": "Nonsteroidal anti-inflammatory medication for pain and inflammation", "kind": "medication" },
{ "summary": "Home mobility exercises and activity modification", "kind": "exercise" },
{ "summary": "Proper body mechanics with lifting and bending; call if pain worsens or leg symptoms develop", "kind": "education" },
{ "summary": "Return visit in two weeks; if no substantial improvement, consider advanced imaging or specialist referral", "kind": "follow_up" }
],
"follow_up": "Return visit in two weeks to reassess pain response to conservative management. Patients should call immediately if pain worsens or new neurological symptoms develop.",
"icd10_candidates": [
{ "code": "M54.5", "label": "Low back pain", "confidence": null }
]
},
"model_tier": "default",
"request_id": "req-xyz",
"timestamp": "2026-05-12T14:23:00.000Z"
}

The example above is a real production response generated from a synthetic two-session transcript covering one week of work-related lower-back pain, called with call_mode: "on_demand" (default). The narrative fields and the structured-extraction values are produced in a single LLM call.

FieldTypeAlways presentDescription
subjectivestringYesPatient's reported history, symptoms, concerns, in clinical voice.
objectivestringYesObserved findings during the visit (vitals, physical exam, prior-result review).
assessmentstringYesClinical judgment integrating Subjective + Objective.
planstringYesNext steps -- orders, prescriptions, referrals, education, follow-up.
structured_extractionobjectYesStructured fields derived from the transcript. Inner arrays may be empty.
structured_extraction.chief_complaintstring | nullNoOne-line presenting complaint.
structured_extraction.vitalsarrayYesVital-sign readings. Up to 32 entries.
structured_extraction.vitals[].namestringYesVital name (e.g. "Blood pressure").
structured_extraction.vitals[].valuestringYesNumeric or qualitative value.
structured_extraction.vitals[].unitstring | nullNoUnit of measure.
structured_extraction.vitals[].tsstring | nullNoTimestamp if recorded mid-encounter.
structured_extraction.assessmentsarrayYesStructured clinical assessments. Up to 32 entries.
structured_extraction.assessments[].summarystringYesAssessment text.
structured_extraction.assessments[].confidencenumber | nullNo[0.0, 1.0].
structured_extraction.plan_itemsarrayYesDiscrete plan items. Up to 32 entries.
structured_extraction.plan_items[].summarystringYesPlan-item text.
structured_extraction.plan_items[].kindstring | nullNoOptional item-type hint (e.g. prescription, referral, education).
structured_extraction.follow_upstring | nullNoFree-text follow-up plan summary.
structured_extraction.icd10_candidatesarrayYesSuggested ICD-10 codes. Up to 32 entries.
structured_extraction.icd10_candidates[].codestringYesICD-10 code.
structured_extraction.icd10_candidates[].labelstringYesHuman-readable label.
structured_extraction.icd10_candidates[].confidencenumber | nullNo[0.0, 1.0].
model_tierstringYesWhich model tier the engine selected for this call. "haiku" when call_mode was refresh, "sonnet" when call_mode was final, "default" for on_demand. Returned so callers can confirm tier selection and reason about latency / quality.
request_idstring | nullNoEcho of the inbound X-Request-ID.
timestampstringYesISO 8601, UTC.

The four narrative fields are guaranteed non-empty -- if the transcript is too thin to support a section, the engine writes the shortest honest sentence (e.g. "No objective findings documented during this visit.") rather than fabricating content. Empty inner arrays in structured_extraction are normal when the transcript is silent on that dimension.

Status codes

CodeMeaningWhen
200OKDraft generated.
400ValidationErrorBody shape, enum violation, invalid call_mode, or both sessions empty AND trimmed_transcript absent.
400DeIdentificationViolationBody carries an identifier-shaped value.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds 512 KB.
415UnsupportedMediaTypeContent-Type is not application/json.
429RateLimitedPer-consumer rate limit exceeded.
500InternalErrorUnexpected failure.
502UpstreamErrorUpstream model call failed, or generated text failed the outbound de-id pass.
503ServiceUnavailableService temporarily unhealthy.
504UpstreamTimeoutUpstream model call exceeded the engine's draft-generation budget.

Examples

curl

curl -sS -X POST "https://engine.tripod-health.com/api/encounters/soap/draft" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessions": [
{
"session_id": "00000000-0000-0000-0000-000000000001",
"started_at": "2026-05-12T14:02:00.000Z",
"segments": [
{
"speaker": "provider",
"speaker_label": "Provider",
"text": "Hello, can you describe what brings you in today?",
"started_at_ms": 0,
"ended_at_ms": 3500
},
{
"speaker": "patient",
"speaker_label": "Patient",
"text": "I have had lower-back pain for about a week.",
"started_at_ms": 3500,
"ended_at_ms": 9000
}
]
}
],
"encounter_meta": {
"visit_type": "initial",
"encounter_type": "in_person"
}
}'

Python (requests)

# Uses the call() helper from quickstart.md.
result = call("POST", "/api/encounters/soap/draft", {
"sessions": [
{
"session_id": "00000000-0000-0000-0000-000000000001",
"started_at": "2026-05-12T14:02:00.000Z",
"segments": [
{
"speaker": "provider",
"speaker_label": "Provider",
"text": "Hello, can you describe what brings you in today?",
"started_at_ms": 0,
"ended_at_ms": 3500,
},
{
"speaker": "patient",
"speaker_label": "Patient",
"text": "I have had lower-back pain for about a week.",
"started_at_ms": 3500,
"ended_at_ms": 9000,
},
],
}
],
"encounter_meta": {
"visit_type": "initial",
"encounter_type": "in_person",
},
})

print("--- Subjective ---")
print(result["subjective"])
print("--- Plan items ---")
for item in result["structured_extraction"]["plan_items"]:
print("-", item["summary"])

Node.js (native fetch)

// Uses the call() helper from quickstart.md.
const result = await call("POST", "/api/encounters/soap/draft", {
sessions: [
{
session_id: "00000000-0000-0000-0000-000000000001",
started_at: "2026-05-12T14:02:00.000Z",
segments: [
{
speaker: "provider",
speaker_label: "Provider",
text: "Hello, can you describe what brings you in today?",
started_at_ms: 0,
ended_at_ms: 3500,
},
{
speaker: "patient",
speaker_label: "Patient",
text: "I have had lower-back pain for about a week.",
started_at_ms: 3500,
ended_at_ms: 9000,
},
],
},
],
encounter_meta: {
visit_type: "initial",
encounter_type: "in_person",
},
});

console.log("Subjective:", result.subjective);
for (const item of result.structured_extraction.plan_items) {
console.log("-", item.summary);
}

Refresh mode with a trimmed transcript (mid-visit periodic draft)

Use call_mode: "refresh" and pass the trimmed_text returned by /api/encounters/transcript/trim as trimmed_transcript. sessions may be empty -- the engine reads the trimmed text as its primary source. This path runs on Haiku.

curl -sS -X POST "https://engine.tripod-health.com/api/encounters/soap/draft" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessions": [],
"encounter_meta": { "visit_type": "initial", "encounter_type": "in_person" },
"call_mode": "refresh",
"trimmed_transcript": "Chief Complaint: Right lower back pain for three weeks, onset after lifting. History of Present Illness: ..."
}'

The response shape is unchanged; the model_tier field will be "haiku".

Final mode with a trimmed transcript (end-of-call wrap-up)

Same shape as refresh but use call_mode: "final". Runs on Sonnet for richer structured extraction (more ICD-10 candidates, typed plan_items[].kind, longer narrative).

curl -sS -X POST "https://engine.tripod-health.com/api/encounters/soap/draft" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessions": [],
"encounter_meta": { "visit_type": "initial", "encounter_type": "in_person" },
"call_mode": "final",
"trimmed_transcript": "Chief Complaint: Right lower back pain for three weeks, onset after lifting. ..."
}'

Expect higher latency on final (Sonnet) than refresh (Haiku) or on_demand (default tier).

Intended use

  • Render the four narrative fields as editable text areas for clinician review and edit before save.
  • Render the structured extraction in a sidebar or chip cluster for quick reference; let the clinician modify or remove items before saving the encounter record.
  • Re-fire the endpoint if the clinician wants a fresh draft after capturing more transcript content (e.g. a re-Started visit added a second session).
  • Wire the periodic mid-visit refresh by calling /api/encounters/transcript/trim every few minutes and feeding its trimmed_text back into this endpoint with call_mode: "refresh". End the visit with one call_mode: "final" call for the richer end-of-call draft.

Mistaken use

  • Persisting the draft verbatim without clinician review. The output is a draft -- it is the clinician's responsibility to edit and attest before it becomes the clinical record.
  • Treating ICD-10 candidates as billing codes. They are suggestions to speed coder review, not authoritative claims data.
  • Sending raw patient identifiers in session_id or speaker_label. Both are opaque to the engine; use consumer-side surrogates and keep the consumer-internal mapping on your side.
  • Asking the endpoint to summarize material that is not in the transcript (lab results, prior visit notes, imaging reports). The prompt is constrained to the transcript; out-of-band material has to flow through a different surface.

De-identification contract

This endpoint enforces both directions of the engine's de-id contract:

  • Inbound: the request body is walked for identifier-shaped strings and blocked field names. Failures return 400 DeIdentificationViolation. See conventions.md.
  • Outbound: the four narrative fields plus every string the structured extraction can carry are scanned for identifier shapes (SSN, email, phone, ISO date, MRN-like patterns). A match fails the call with 502 UpstreamError; the engine never silently scrubs.

Rate limits

Per-consumer limits are managed at the gateway. See rate-limits.md.

Change history

DateChange
2026-05-12Initial publication.
2026-05-26Added call_mode (on_demand / refresh / final) and trimmed_transcript request fields for the periodic-refresh + end-of-call wrap-up pattern. sessions may now be empty when trimmed_transcript carries the content. Response gains model_tier reporting which tier the engine selected.