The Mobile API provides a job-based async endpoint optimized for mobile applications. Unlike the web SSE endpoint, the mobile API handles interruptions gracefully (phone calls, app backgrounding, network switches) by maintaining job state server-side.
POST /api/mobile/generate-note¶Submit a note generation job and receive a job_id immediately.
Request:
{
"note_types": ["soap", "triage_note", "admin_note"],
"specialty": "emergency",
"transcript": "Patient presents with...",
"visiting_id": "visit-12345",
"user_email": "dr.smith@hospital.com",
"mrn_id": "MRN-67890"
}
Response (immediate):
{
"job_id": "92935021-3a0c-4e2f-9709-7cc97e6c25f5",
"status": "queued",
"estimated_time_seconds": 26,
"poll_url": "/api/mobile/jobs/92935021-3a0c-4e2f-9709-7cc97e6c25f5"
}
GET /api/mobile/jobs/{job_id}¶Poll for job status and retrieve results when complete.
Response (processing):
{
"job_id": "92935021-3a0c-4e2f-9709-7cc97e6c25f5",
"status": "processing",
"progress": 45,
"current_step": "Generating notes in parallel",
"notes_completed": 1,
"notes_total": 3,
"created_at": "2025-11-02T12:30:41.575925"
}
Response (complete):
{
"job_id": "92935021-3a0c-4e2f-9709-7cc97e6c25f5",
"status": "complete",
"progress": 100,
"current_step": "Complete",
"notes_completed": 3,
"notes_total": 3,
"notes": [
{
"note_type": "soap",
"note": "**Subjective:**\n- Patient presents...",
"validation": {
"validation_score": 0.85,
"passed": true,
"validators_used": 6,
"checks": {
"terminology": {"score": 0.90, "passed": true},
"completeness": {"score": 0.80, "passed": true},
"format": {"score": 1.00, "passed": true},
"coherence": {"score": 0.85, "passed": true},
"accuracy": {"score": 0.88, "passed": true},
"semantic": {"score": 0.92, "passed": true}
}
}
}
],
"session_id": "abc-123-xyz",
"processing_time_seconds": 28.5,
"created_at": "2025-11-02T12:30:41.575925",
"completed_at": "2025-11-02T12:31:10.123456"
}
Response (failed):
{
"job_id": "92935021-3a0c-4e2f-9709-7cc97e6c25f5",
"status": "failed",
"progress": 50,
"notes_completed": 1,
"notes_total": 3,
"errors": [
{
"note_type": "discharge_summary",
"error": "Timeout generating note"
}
],
"created_at": "2025-11-02T12:30:41.575925",
"completed_at": "2025-11-02T12:31:15.000000"
}
MobileGenerateNoteRequest¶| Field | Type | Required | Description |
|---|---|---|---|
note_types |
Array[String] | Yes | Note types to generate (e.g., ["soap", "triage_note"]) |
specialty |
String | No | Medical specialty (emergency, urology, general_practice, pediatrics, general). Auto-detects if omitted |
transcript |
String | Yes | Patient transcript (alias: transcription) |
visiting_id |
String | Yes | Visit identifier for historical aggregation |
user_email |
Yes | User email for personalization (alias: user_email_address) | |
mrn_id |
String | No | Medical Record Number |
Available Note Types:
- soap - SOAP Note
- progress_note - Progress Note
- triage_note - Triage Note
- ed_note - ED Note
- ed_assessment - ED Assessment
- nursing_note - Nursing Note
- admin_note - Admin Note
- referral_letter - Referral Letter
- discharge_summary - Discharge Summary
- procedure - Procedure Note
Available Specialties:
- emergency - Emergency Department
- urology - Urology
- general_practice - General Practice
- pediatrics - Pediatrics
- general - General Medicine
The mobile API is designed to handle real-world mobile interruptions:
1. Client submits job → Receives job_id → Disconnects
2. User receives phone call (app backgrounds)
3. Job continues running on server independently
4. User returns 2 minutes later
5. Client polls with job_id → Receives completed notes
✅ Phone calls - Job continues while user is on call ✅ App backgrounding - iOS/Android suspends app, job continues ✅ Network switches - WiFi ↔ 4G transitions, client can reconnect ✅ Screen lock - Device locks, job continues ✅ App switching - User switches to another app ✅ Network loss - Temporary network outage, client can reconnect
queued → processing → complete (or failed)
↓ ↓ ↓
0-5s 5-40s Retrieved
↓
Deleted after 60s
Recommended polling pattern:
async function generateNotesMobile(request) {
// Step 1: Submit job
const submitResponse = await fetch('/api/mobile/generate-note', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request)
});
const {job_id, estimated_time_seconds} = await submitResponse.json();
// Step 2: Poll until complete (with exponential backoff)
let pollInterval = 2000; // Start with 2 seconds
const maxInterval = 10000; // Max 10 seconds
while (true) {
await sleep(pollInterval);
const statusResponse = await fetch(`/api/mobile/jobs/${job_id}`);
const status = await statusResponse.json();
if (status.status === 'complete') {
return status.notes; // Success!
} else if (status.status === 'failed') {
throw new Error('Job failed');
}
// Increase poll interval (exponential backoff)
pollInterval = Math.min(pollInterval * 1.5, maxInterval);
// Update UI with progress
updateProgress(status.progress, status.current_step);
}
}
try {
const notes = await generateNotesMobile(request);
displayNotes(notes);
} catch (error) {
if (error.message.includes('404')) {
// Job not found (expired or invalid job_id)
alert('Session expired. Please try again.');
} else if (error.message.includes('timeout')) {
// Network timeout - job might still be running
alert('Network timeout. Check your connection and try polling again.');
} else {
alert(`Error: ${error.message}`);
}
}
func generateNotes() async throws -> [Note] {
// Submit job
let submitRequest = GenerateNoteRequest(
noteTypes: ["soap", "triage_note"],
specialty: "emergency",
transcript: transcript,
visitingId: "visit-001",
userEmail: "dr.smith@hospital.com"
)
let submitResponse = try await apiClient.post(
"/api/mobile/generate-note",
body: submitRequest
)
let jobId = submitResponse.jobId
// Poll until complete
while true {
try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
let status = try await apiClient.get("/api/mobile/jobs/\(jobId)")
if status.status == "complete" {
return status.notes
} else if status.status == "failed" {
throw NSError(domain: "JobFailed", code: 1)
}
// Update progress
updateProgress(status.progress)
}
}
suspend fun generateNotes(): List<Note> = withContext(Dispatchers.IO) {
// Submit job
val submitRequest = GenerateNoteRequest(
noteTypes = listOf("soap", "triage_note"),
specialty = "emergency",
transcript = transcript,
visitingId = "visit-001",
userEmail = "dr.smith@hospital.com"
)
val submitResponse = apiClient.post<JobSubmitResponse>(
"/api/mobile/generate-note",
body = submitRequest
)
val jobId = submitResponse.jobId
// Poll until complete
while (true) {
delay(3000) // 3 seconds
val status = apiClient.get<JobStatusResponse>("/api/mobile/jobs/$jobId")
when (status.status) {
"complete" -> return@withContext status.notes!!
"failed" -> throw Exception("Job failed")
else -> updateProgress(status.progress)
}
}
}
import {useEffect, useState} from 'react';
function useGenerateNotes(request) {
const [status, setStatus] = useState(null);
const [notes, setNotes] = useState(null);
useEffect(() => {
let polling = true;
let pollInterval;
async function submitAndPoll() {
// Submit
const submitRes = await fetch('/api/mobile/generate-note', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request)
});
const {job_id} = await submitRes.json();
// Poll
pollInterval = setInterval(async () => {
if (!polling) return;
const statusRes = await fetch(`/api/mobile/jobs/${job_id}`);
const jobStatus = await statusRes.json();
setStatus(jobStatus);
if (jobStatus.status === 'complete') {
setNotes(jobStatus.notes);
polling = false;
clearInterval(pollInterval);
}
}, 3000);
}
submitAndPoll();
// Cleanup on unmount
return () => {
polling = false;
if (pollInterval) clearInterval(pollInterval);
};
}, [request]);
return {status, notes};
}
| Feature | Web (SSE) | Mobile (Job-based) |
|---|---|---|
| Endpoint | POST /api/generate-note | POST /api/mobile/generate-note + GET /api/mobile/jobs/{id} |
| Response Type | Streaming (SSE) | JSON (polling) |
| Connection | Long-held (30-40s) | Short requests (reconnect-able) |
| Interruption Handling | ❌ Connection drops | ✅ Job continues independently |
| Phone Call | ❌ Job lost | ✅ Job survives |
| App Background | ❌ Connection suspended | ✅ Job continues |
| Network Switch | ❌ Connection drops | ✅ Reconnect anytime |
| Battery Impact | Higher (long connection) | Lower (short polls) |
| Latency | Real-time tokens | Poll delay (2-5s) |
| Parallel Notes | ✅ Yes | ✅ Yes |
| Rich Validation | ✅ Yes | ✅ Yes |
| Best For | Desktop/web browsers | Native mobile apps |
| Status Code | Meaning | Action |
|---|---|---|
| 200 | Success | Continue polling or retrieve notes |
| 404 | Job not found | Job expired (>1 hour) or invalid job_id |
| 422 | Validation error | Check request format |
| 500 | Server error | Retry submission |
# Submit job
curl -X POST http://localhost:8002/api/mobile/generate-note \
-H "Content-Type: application/json" \
-d '{
"note_types": ["soap"],
"transcript": "Patient has pain...",
"visiting_id": "visit-001",
"user_email": "test@test.com"
}'
# Get job_id from response, then poll after 30 seconds
curl http://localhost:8002/api/mobile/jobs/{job_id}
// In mobile Chrome/Safari
async function testMobile() {
const submit = await fetch('/api/mobile/generate-note', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
note_types: ['soap'],
transcript: 'Patient has dysuria for 3 days',
visiting_id: 'test-001',
user_email: 'test@test.com'
})
});
const {job_id} = await submit.json();
console.log('Job ID:', job_id);
// Lock screen, make a call, or switch apps - job continues!
// Poll after interruption
const result = await fetch(`/api/mobile/jobs/${job_id}`);
console.log(await result.json());
}
The mobile API is included in the same Docker container as the web API: - Both APIs share the same backend services - No additional infrastructure needed - Same ECS deployment
Jobs are stored in-memory, so: - Single instance: Up to ~1000 concurrent jobs - Multiple instances: Need Redis/DynamoDB for shared job state - Current limit: 1-hour job retention, auto-cleanup
Check job manager health:
# In server logs
grep "Job manager" /tmp/uvicorn_8002.log
# Expected:
# Job manager initialized (in-memory)
# Job manager cleanup task started
# Created job abc-123: 3 notes for visit-001
# Job abc-123: All 3 notes completed successfully
✅ Survives interruptions - Phone calls, app backgrounding, network switches ✅ Battery efficient - Short polling vs long-held connection ✅ Network resilient - Can reconnect anytime with job_id ✅ iOS/Android friendly - No EventSource polyfill needed ✅ Works in background - Job continues even when app suspended ✅ Simpler implementation - Standard REST, no streaming complexity
Q: What happens if my app crashes during generation? A: The job continues running. Reopen the app and poll with the same job_id to get results.
Q: How long are jobs stored? A: Jobs are stored for 1 hour or 60 seconds after result retrieval (whichever comes first).
Q: Can I poll the same job multiple times? A: Yes, until it's deleted. The first retrieval schedules deletion after 60 seconds.
Q: What if I lose the job_id? A: Job is lost. There's no endpoint to list jobs (for security/privacy). Store job_id locally.
Q: Can I cancel a job? A: Not currently supported. Job will complete and auto-delete after 1 hour.
Q: Does it support the same features as web API? A: Yes! Parallel multi-note generation, specialty selection, full validation, user examples, historical aggregation.