Skip to content

Real-Time SOAP Streaming

Real-Time SOAP Streaming

Stream structured clinical documentation as it is generated. Subjective, Objective, Assessment, and Plan sections arrive progressively via Server-Sent Events — your UI updates in real time as each section completes.


Quick Start

Get real-time SOAP notes in under 15 lines of JavaScript:

const response = await fetch('/v1/consultation/progressive-soap/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
consultation_id: 'consult-abc-123',
accumulated_text: 'Patient reports headache for 3 days, tension type, no nausea. Denies fever. BP 120/80. HR 72. Normal neurological exam.',
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split('\n\n');
buffer = blocks.pop();
for (const block of blocks) {
const evt = block.match(/^event:\s*(.+)$/m)?.[1];
const data = block.match(/^data:\s*(.+)$/m)?.[1];
if (evt && data) console.log(evt, JSON.parse(data));
}
}

Architecture

The streaming flow is designed for low latency and intelligent caching. Your client sends the full accumulated transcript, and DELPHOS decides the most efficient path to deliver structured SOAP sections.

Your Client DELPHOS SSE Events Live UI
──────────── ──► ─────────────────── ──► ──────────────────── ──► ────────
Send full Cache check + status → partial Real-time
accumulated generation → soap (or error) section
text Smart 5-word delta updates

Endpoint Reference

POST /v1/consultation/progressive-soap/stream

Request Headers

HeaderValueRequired
Content-Typeapplication/jsonYes
x-api-keyYour API keyYes
Accepttext/event-streamRecommended

Request Body

FieldTypeRequiredDescription
consultation_idstringYesExternal consultation identifier (1–255 chars)
accumulated_textstringNoFull transcript accumulated so far (max 100,000 chars). Always send the COMPLETE text each time, not just new chunks. Null is coerced to empty string. Default: ""
previous_soap_hashstring | nullNoSHA-256 hash from the last response’s soap_hash field. Enables cache optimization. Default: null
streambooleanNoIf true (default), return SSE stream. If false, return standard JSON response.

Example Request

{
"consultation_id": "consult-abc-123",
"accumulated_text": "Paciente relata cefaleia há 3 dias, tipo tensional, sem náuseas. Nega febre. PA 120/80. FC 72. Exame neurológico normal.",
"previous_soap_hash": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
"stream": true
}

SSE Event Types

The stream emits four event types, always in order: status first, then zero or more partial events, and finally either soap (success) or error.

status — Processing Status

Indicates whether DELPHOS is returning a cached result or generating new content.

event: status
data: {"type": "cache_hit"}
event: status
data: {"type": "generating"}
TypeMeaning
cache_hitPrevious result returned from cache — no processing needed
generatingNew SOAP content is being generated

partial — Intermediate SOAP

Delivers partially-complete SOAP sections as they are generated. The completeness object indicates which sections are finished. You may receive multiple partial events as more sections complete.

event: partial
data: {
"subjective": "Paciente relata cefaleia há 3 dias, tipo tensional...",
"objective": "PA 120/80, FC 72, exame neurológico normal.",
"assessment": "",
"plan": "",
"completeness": { "s": true, "o": true, "a": false, "p": false }
}
FieldTypeDescription
subjectivestringPatient’s reported symptoms and history
objectivestringClinical findings and measurements
assessmentstringClinical assessment and diagnosis
planstringTreatment plan and next steps
completenessobject{ s, o, a, p } — boolean flags for each section

soap — Final Complete SOAP

The definitive response with all four sections complete. Includes soap_hash for cache optimization on subsequent requests.

event: soap
data: {
"subjective": "Paciente relata cefaleia há 3 dias, tipo tensional, sem náuseas ou vômitos. Nega febre, alterações visuais ou trauma recente.",
"objective": "PA 120/80 mmHg. FC 72 bpm. Exame neurológico normal. Sem sinais meníngeos.",
"assessment": "Cefaleia tensional sem sinais de alarme. Sem indicação de investigação complementar neste momento.",
"plan": "Orientação sobre higiene do sono e manejo de estresse. Analgésico simples (dipirona 500mg) SOS. Retorno se piora ou surgimento de novos sintomas.",
"completeness": { "s": true, "o": true, "a": true, "p": true },
"soap_hash": "a3f8c2d91b4e5678901234567890abcdef1234567890abcdef1234567890ab12",
"cached": false,
"is_degraded": false
}

error — Error

Sent when processing fails. Includes a machine-readable code and a descriptive message. When degraded is true, a best-effort partial SOAP may have been emitted before the error.

event: error
data: {
"code": "PROCESSING_TIMEOUT",
"message": "Processing request timed out",
"degraded": true
}

Integration Patterns

Choose the integration approach that best fits your stack. All patterns handle the full event lifecycle.

/**
* Stream SOAP notes via POST + manual SSE parsing.
* (EventSource only supports GET, so we use fetch + ReadableStream.)
*/
async function streamSOAP(consultationId, text, prevHash, onEvent) {
const response = await fetch('/v1/consultation/progressive-soap/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
consultation_id: consultationId,
accumulated_text: text,
previous_soap_hash: prevHash,
stream: true
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split('\n\n');
buffer = blocks.pop(); // keep incomplete block
for (const block of blocks) {
if (!block.trim()) continue;
const evtLine = block.match(/^event:\s*(.+)$/m);
const dataLine = block.match(/^data:\s*(.+)$/m);
if (evtLine && dataLine) {
onEvent(evtLine[1].trim(), JSON.parse(dataLine[1]));
}
}
}
}
// Usage
let lastHash = null;
streamSOAP('consult-123', fullText, lastHash, (type, data) => {
switch (type) {
case 'status':
console.log('Status:', data.type); // "cache_hit" or "generating"
break;
case 'partial':
updateSOAPDisplay(data); // show sections as they arrive
break;
case 'soap':
updateSOAPDisplay(data);
lastHash = data.soap_hash; // save for next request
break;
case 'error':
console.error(data.code, data.message);
break;
}
});

Notes-Based Flow

When the physician types clinical notes directly, use a debounce pattern to avoid excessive requests. After each pause in typing, send the full accumulated text to DELPHOS.

Doctor Types → Debounce (1.5s) → Send Full Text → SOAP Builds Live
let debounceTimer = null;
let lastHash = null;
textarea.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
streamSOAP(
consultationId,
textarea.value, // always the FULL text
lastHash,
(type, data) => {
if (type === 'partial' || type === 'soap') {
updateSOAPDisplay(data);
if (data.soap_hash) lastHash = data.soap_hash;
}
}
);
}, 1500);
});

Audio-Based Flow

For voice-driven consultations, your application handles audio capture and speech-to-text conversion. Feed the growing transcript buffer into DELPHOS as it accumulates.

Audio Chunks → Your Transcription → Accumulated Text → DELPHOS SOAP
(microphone) (speech-to-text) (growing buffer) (SSE stream)
let transcriptBuffer = '';
let soapHash = null;
// Called whenever your transcription service produces new text
function onTranscriptChunk(newText) {
transcriptBuffer += ' ' + newText;
streamSOAP(
consultationId,
transcriptBuffer, // always the COMPLETE buffer
soapHash,
(type, data) => {
if (type === 'soap') {
updateSOAPDisplay(data);
soapHash = data.soap_hash;
} else if (type === 'partial') {
updateSOAPDisplay(data);
}
}
);
}

Error Handling

When DELPHOS encounters an issue during generation, an error event is emitted. Your application should handle each code and implement retry with backoff.

Error CodeDescriptionRetry?
PROCESSING_TIMEOUTProcessing exceeded the time limit. A previously streamed partial event may contain usable content.Yes, with backoff
PROCESSING_ERRORInternal processing failure.Yes, after short delay
PARSE_ERROROutput could not be parsed into SOAP sections.Retry once — if persistent, input may need refinement
INTERNAL_ERRORUnexpected server error.Yes, with backoff

Retry with Exponential Backoff

async function streamWithRetry(consultationId, text, hash, onEvent, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
await streamSOAP(consultationId, text, hash, (type, data) => {
if (type === 'error' && !['PARSE_ERROR', 'PROCESSING_TIMEOUT', 'PROCESSING_ERROR'].includes(data.code)) {
throw new Error(data.code);
}
onEvent(type, data);
});
return; // success
} catch (err) {
attempt++;
if (attempt >= maxRetries) {
// Fallback to non-streaming endpoint
const res = await fetch('/v1/consultation/progressive-soap', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
consultation_id: consultationId,
accumulated_text: text,
previous_soap_hash: hash
})
});
const data = await res.json();
onEvent('soap', data);
return;
}
// Exponential backoff: 1s, 2s, 4s
await new Promise(r => setTimeout(r, 1000 * 2 ** (attempt - 1)));
}
}
}

FAQ

Do I need to save SOAP notes to my database? No. DELPHOS handles all persistence automatically. Your application only needs to display the SOAP sections as they stream in. If you need historical access, query the DELPHOS consultation API.

How often should I send requests? Send requests on meaningful changes (e.g., after a typing pause). DELPHOS has a built-in 5-word delta threshold that automatically returns cached results when the text has not changed significantly.

What is previous_soap_hash? A SHA-256 hash returned in the final soap event. Pass it back on subsequent requests to enable cache optimization. If the text has not changed enough to warrant regeneration, DELPHOS returns the cached result instantly.

Can I send accumulated_text as null? Yes. A null value is automatically coerced to an empty string (""). However, an empty string will not produce meaningful SOAP output.

Should I send only new text or the full text? Always send the full accumulated text. DELPHOS needs the complete clinical narrative to generate accurate, contextual SOAP notes. Sending only deltas results in incomplete or incoherent output.

How do I switch to non-streaming mode? Set "stream": false in the request body, or use the non-streaming endpoint POST /v1/consultation/progressive-soap directly. Both return the same SOAP structure as a single JSON response.