Suggest Nudges
Status:
stableCategory:trinityassistAdded:2026-05-26Last 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
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/nudge/suggest |
| Full URL | https://engine.tripod-health.com/api/nudge/suggest |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No (each call may produce a fresh candidate set) |
| Rate-limited | Yes |
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
| Header | Required | Notes |
|---|---|---|
apikey | Yes | Your engine API key. |
Content-Type | Yes | application/json. |
X-Request-ID | No | Optional 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
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
encounter_id | string | No | 1-64 chars | Optional 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_window | array | Yes | 1-256 segments | Finalized transcript segments to inspect. |
transcript_window[].segment_id | string | Yes | 1-256 chars | Consumer-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[].speaker | string | Yes | 1-32 chars | One of provider, patient, other, unknown (or any speaker label your transcription source emits). |
transcript_window[].text | string | Yes | 1-8,000 chars | Utterance text. |
transcript_window[].started_at_ms | integer | Yes | >=0 | Offset (ms) from session start. |
transcript_window[].ended_at_ms | integer | Yes | >=0 | Offset (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
| Field | Type | Always present | Description |
|---|---|---|---|
candidates | array | Yes | Zero or more candidate nudges. An empty array is a valid response and means the model found nothing worth nudging on. |
candidates[].card_id | string | Yes | Engine-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[].category | string | Yes | Kebab-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[].body | string | Yes | Short imperative copy, max 280 chars. Never HTML, never markup. Suitable to render verbatim in a clinician-facing card. |
candidates[].severity | enum | Yes | One of info, suggestion, reminder. |
candidates[].triggered_by_segment_ids | array | Yes | Subset 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_id | string | null | Yes | Echo of the inbound X-Request-ID if present, otherwise null. |
timestamp | string | Yes | ISO 8601 UTC server timestamp at response emission. |
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Candidates generated (zero or more). |
400 | ValidationError | Body shape failure -- missing or empty transcript_window, malformed segment, wrong types. field names the offender. |
400 | DeIdentificationViolation | Body carries an identifier-shaped value. |
401 | Unauthenticated | Missing or invalid API key. |
413 | PayloadTooLarge | Body exceeds 512 KB. |
415 | UnsupportedMediaType | Content-Type is not application/json. |
422 | UnprocessableEntity | A downstream Pydantic constraint rejected the body (typically an unknown extra field; the request schema uses extra="forbid"). |
429 | RateLimited | Per-consumer rate limit exceeded. |
500 | InternalError | Unexpected failure. |
502 | UpstreamError | Upstream model call failed, or generated text failed the outbound de-id pass. |
503 | ServiceUnavailable | Service 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
categoryslug 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 yieldpain-assessment-missingandpain-severity-missingin different calls. Render thebodyverbatim, 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_resolutionto 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
- Capture the
request_idfrom the response (success or error) or theX-Request-IDresponse header. Every support ticket starts here. - Empty
candidatesarray -- 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. 400 ValidationErrorwithfield-- the named field is malformed. Most common:transcript_windowempty or missing, segment missingstarted_at_ms/ended_at_ms, or a segmenttextexceeding 8,000 chars.422 UnprocessableEntity-- the request schema isextra="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.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
bodyis scanned for identifier shapes (SSN, email, phone, ISO date, MRN-like patterns); a match fails the call with502 UpstreamError. The engine never silently scrubs.
See conventions.md.
Rate limits
Per-consumer limits are enforced at the gateway. See rate-limits.md.
Related
POST /api/nudge/check_resolution-- companion endpoint that decides which currently-open nudges a transcript window has resolved.- conventions.md -- de-identification contract, request IDs, timestamp shape.
- errors.md -- error envelope and code list.
Change history
| Date | Change |
|---|---|
| 2026-05-26 | Initial 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-26 | Resource 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. |