Loading documentation

Access required

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

Sign out
Skip to main content

Suggest Nudges

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

Summary

Inspect a window of finalized transcript segments and return zero or more clinical-decision nudge candidates. Each candidate carries a kebab-case category slug, a short imperative body, a severity, and the segment ids the model cited as the trigger. Empty arrays are common and correct -- most short windows do not warrant a nudge.

This is the engine's only nudge surface. It is a stateless HTTP endpoint: the consumer feeds a window of finalized transcript segments and gets back candidates. The consumer owns cadence and transcript capture (the engine no longer streams transcription); a live-visit integration typically calls this on a recurring window during the call.

Nudge bodies are AI-generated within engine guardrails. There is no fixed prompt registry -- the predecessor /api/nudge/prompt endpoint has been retired.

Endpoint

FieldValue
MethodPOST
Path/api/nudge/suggest
Full URLhttps://engine.tripod-health.com/api/nudge/suggest
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo (each call may produce a fresh candidate set)
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.

Purpose

The consumer drives this endpoint over the course of a clinical encounter -- typically on a recurring window during a live visit, but equally for a one-shot pass over a recorded transcript. Common uses:

  • Driving the suggestion pass on a cadence during a live visit, feeding the most recent transcript window each time and surfacing the returned candidates to the clinician.
  • Re-running the same logic over a recorded transcript window for evaluation, regression testing, or A/B prompt work.

Cadence, deduplication of repeated candidates, and resolution tracking are the consumer's responsibility; pair this with check_resolution to retire satisfied nudges.

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-IDNoOptional client correlation ID. Echoed back in the response.

Body

{
"encounter_id": "visit-2026-05-26-shoulder-01",
"transcript_window": [
{
"segment_id": "s1",
"speaker": "provider",
"text": "Tell me what is going on today.",
"started_at_ms": 0,
"ended_at_ms": 3000
},
{
"segment_id": "s2",
"speaker": "patient",
"text": "My lower back has been killing me ever since I helped my brother move couches a few weeks back.",
"started_at_ms": 3000,
"ended_at_ms": 9000
},
{
"segment_id": "s3",
"speaker": "patient",
"text": "It is worst in the morning and sometimes shoots into my right leg.",
"started_at_ms": 9000,
"ended_at_ms": 14000
}
]
}

Body fields

FieldTypeRequiredConstraintsDescription
encounter_idstringNo1-64 charsOptional opaque correlation key. Send the same value on a later POST /api/nudge/check_resolution call to keep both calls grouped under the same visit in your own logs. Treated as opaque by the engine.
transcript_windowarrayYes1-256 segmentsFinalized transcript segments to inspect.
transcript_window[].segment_idstringYes1-256 charsConsumer-supplied id. Treated as opaque by the engine. The streaming inferencer uses the shape <correlation_id>:<seq_no>; standalone callers can use any non-empty value.
transcript_window[].speakerstringYes1-32 charsOne of provider, patient, other, unknown (or any speaker label your transcription source emits).
transcript_window[].textstringYes1-8,000 charsUtterance text.
transcript_window[].started_at_msintegerYes>=0Offset (ms) from session start.
transcript_window[].ended_at_msintegerYes>=0Offset (ms) from session start.

The body MUST NOT contain identifier-shaped values. The engine's inbound de-identification posture rejects any payload that carries names, dates of birth, SSN-shaped values, phone numbers, email addresses, MRNs, or similar identifiers. Strip identifiers on the consumer side before calling. See conventions.md.

Response

Success -- 200 OK

{
"candidates": [
{
"card_id": "01KSJTM039H6H6Z5PJQSDD2V30",
"category": "pain-assessment-missing",
"body": "Ask about pain severity on a 0-10 scale.",
"severity": "suggestion",
"triggered_by_segment_ids": ["s2", "s3"]
},
{
"card_id": "01KSJTM03G3BQ299M77N8TQ017",
"category": "functional-impact-missing",
"body": "Ask how the back pain is affecting daily activities, work, or sleep.",
"severity": "suggestion",
"triggered_by_segment_ids": ["s2", "s3"]
},
{
"card_id": "01KSJTM03PCKK5QH25SQXXFXYV",
"category": "symptom-characterization-gap",
"body": "Clarify the characteristics of the shooting leg pain - onset, frequency, and relation to movement or position.",
"severity": "suggestion",
"triggered_by_segment_ids": ["s3"]
}
],
"request_id": "0926afd2-e6a7-4303-841b-667775135b87",
"timestamp": "2026-05-26T18:19:47.565951+00:00"}

The example above is a real production response generated from a short three-segment window where the patient described low back pain without quantifying severity or functional impact. The model identified three reasonable nudges and grounded each one in the specific segments that justified it.

Response fields

FieldTypeAlways presentDescription
candidatesarrayYesZero or more candidate nudges. An empty array is a valid response and means the model found nothing worth nudging on.
candidates[].card_idstringYesEngine-issued 26-char Crockford-base32 ULID. Unique per candidate, stable for the lifetime of the card. Echo it back as the card_id on a later POST /api/nudge/check_resolution call to evaluate whether the conversation has since satisfied this card.
candidates[].categorystringYesKebab-case slug (matches ^[a-z0-9-]+$, max 64 chars). Stable enough across calls that consumers can deduplicate or cluster by category. The model picks the slug from observation; there is no fixed enumeration.
candidates[].bodystringYesShort imperative copy, max 280 chars. Never HTML, never markup. Suitable to render verbatim in a clinician-facing card.
candidates[].severityenumYesOne of info, suggestion, reminder.
candidates[].triggered_by_segment_idsarrayYesSubset of the segment_id values from the request the model cited as the trigger. May be empty when the model cannot point to a specific segment (rare).
request_idstring | nullYesEcho of the inbound X-Request-ID if present, otherwise null.
timestampstringYesISO 8601 UTC server timestamp at response emission.

Status codes

CodeMeaningWhen
200OKCandidates generated (zero or more).
400ValidationErrorBody shape failure -- missing or empty transcript_window, malformed segment, wrong types. field names the offender.
400DeIdentificationViolationBody carries an identifier-shaped value.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds 512 KB.
415UnsupportedMediaTypeContent-Type is not application/json.
422UnprocessableEntityA downstream Pydantic constraint rejected the body (typically an unknown extra field; the request schema uses extra="forbid").
429RateLimitedPer-consumer rate limit exceeded.
500InternalErrorUnexpected failure.
502UpstreamErrorUpstream model call failed, or generated text failed the outbound de-id pass.
503ServiceUnavailableService temporarily unhealthy.

Error envelope

{
"timestamp": "2026-05-26T18:20:17.071Z",
"error": "ValidationError",
"message": "Field 'transcript_window' must be an array",
"field": "transcript_window",
"request_id": "9f0c2050-6ecf-407e-a1fe-bcb5ce96d3aa"
}

See errors.md for the full error code list.

Examples

curl

curl -sS -X POST "https://engine.tripod-health.com/api/nudge/suggest" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"encounter_id": "visit-2026-05-26-shoulder-01",
"transcript_window": [
{"segment_id": "s1", "speaker": "provider", "text": "Tell me what is going on today.", "started_at_ms": 0, "ended_at_ms": 3000},
{"segment_id": "s2", "speaker": "patient", "text": "My lower back has been killing me since I helped my brother move couches a few weeks back.", "started_at_ms": 3000, "ended_at_ms": 9000},
{"segment_id": "s3", "speaker": "patient", "text": "It is worst in the morning and sometimes shoots into my right leg.", "started_at_ms": 9000, "ended_at_ms": 14000}
]
}'

encounter_id is optional. Omit it for one-shot calls where you do not need to correlate a later resolution check; include it when chaining /api/nudge/suggest -> /api/nudge/check_resolution.

Python (requests)

import os, requests

resp = requests.post(
"https://engine.tripod-health.com/api/nudge/suggest",
headers={"apikey": os.environ["ENGINE_API_KEY"]},
json={
"encounter_id": "visit-2026-05-26-shoulder-01",
"transcript_window": [
{"segment_id": "s1", "speaker": "provider", "text": "Tell me what is going on today.",
"started_at_ms": 0, "ended_at_ms": 3000},
{"segment_id": "s2", "speaker": "patient",
"text": "My lower back has been killing me since I helped my brother move couches a few weeks back.",
"started_at_ms": 3000, "ended_at_ms": 9000},
{"segment_id": "s3", "speaker": "patient",
"text": "It is worst in the morning and sometimes shoots into my right leg.",
"started_at_ms": 9000, "ended_at_ms": 14000},
],
},
timeout=30,
)
resp.raise_for_status()
for candidate in resp.json()["candidates"]:
print(f"[{candidate['severity']}] {candidate['card_id']} {candidate['category']}: {candidate['body']}")

Node.js (fetch)

const resp = await fetch("https://engine.tripod-health.com/api/nudge/suggest", {
method: "POST",
headers: {
apikey: process.env.ENGINE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
encounter_id: "visit-2026-05-26-shoulder-01",
transcript_window: [
{ segment_id: "s1", speaker: "provider", text: "Tell me what is going on today.",
started_at_ms: 0, ended_at_ms: 3000 },
{ segment_id: "s2", speaker: "patient",
text: "My lower back has been killing me since I helped my brother move couches a few weeks back.",
started_at_ms: 3000, ended_at_ms: 9000 },
{ segment_id: "s3", speaker: "patient",
text: "It is worst in the morning and sometimes shoots into my right leg.",
started_at_ms: 9000, ended_at_ms: 14000 },
],
}),
});
if (!resp.ok) throw new Error(`Engine ${resp.status}: ${await resp.text()}`);
const { candidates } = await resp.json();
for (const c of candidates) {
console.log(`[${c.severity}] ${c.card_id} ${c.category}: ${c.body}`);
}

Intended use

  • One-shot HTTP probes over a transcript window outside a live WebSocket session -- batch re-evaluation, offline review of a captured visit, or a non-streaming clinical app.
  • Regression and evaluation harnesses that need to drive the same primitive the streaming inferencer uses, deterministic across reruns insofar as the underlying model is.
  • Building blocks for a custom UI lifecycle outside the engine's built-in WS cadence (e.g. surfacing candidates one at a time through a different UX).

Mistaken use

  • Polling this endpoint inside an open transcription WS session alongside the engine's own nudge stream. The streaming inferencer already calls this primitive every 3 minutes per session; adding a parallel HTTP poll doubles the LLM cost without producing different output.
  • Treating the category slug as an enumeration to switch on. The slug is model-generated and stable in spirit (categories tend to cluster) but not stable in literal value -- two semantically equivalent windows can yield pain-assessment-missing and pain-severity-missing in different calls. Render the body verbatim, group by category for de-duplication.
  • Asking the endpoint to resolve previously-emitted nudges. The suggestion pass does not consider open nudges; it only proposes new ones. Use POST /api/nudge/check_resolution to ask whether a window has resolved an open nudge.
  • Sending raw patient identifiers in segment text or segment_id. Strip identifiers on the consumer side before calling.

Debugging

  1. Capture the request_id from the response (success or error) or the X-Request-ID response header. Every support ticket starts here.
  2. Empty candidates array -- not an error. Short or clinically-thin windows commonly yield zero suggestions. Widen the window or check that segments carry clinical content before assuming the endpoint is broken.
  3. 400 ValidationError with field -- the named field is malformed. Most common: transcript_window empty or missing, segment missing started_at_ms / ended_at_ms, or a segment text exceeding 8,000 chars.
  4. 422 UnprocessableEntity -- the request schema is extra="forbid". Extra fields on segments (e.g. is_final, seq_no) will trip this even though they are harmless semantically. Send only the documented fields.
  5. 502 UpstreamError -- either the model call failed, or the generated text failed the outbound de-id scan. The latter indicates the model produced an identifier-shaped string from a window that already contained one. Strip identifiers from the input transcript and retry.

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; failures return 400 DeIdentificationViolation.
  • Outbound: every generated body is 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.

See conventions.md.

Rate limits

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

Change history

DateChange
2026-05-26Initial publication. Replaces the retired GET /api/nudge/prompt registry lookup. Nudge bodies are now AI-generated within engine guardrails instead of resolved from a fixed prompt catalog.
2026-05-26Resource version bumped to 0.3.0. Response now carries an engine-issued card_id (26-char ULID) on every candidate; echo it back as the card_id on POST /api/nudge/check_resolution to evaluate resolution later. Request body gains an optional encounter_id (max 64 chars) to correlate suggest -> check_resolution calls in your own logs. Both additions are non-breaking.