Mobile API Guide

Overview

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.


Endpoints

1. Submit Job: 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"
}

2. Poll Status: 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"
}

Request Schema

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 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


Interruption Handling

How It Works

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

Supported Interruptions

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

Job Lifecycle

queued → processing → complete (or failed)
  ↓         ↓            ↓
 0-5s     5-40s      Retrieved
                         ↓
                    Deleted after 60s

Mobile Client Implementation

Polling Strategy

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);
  }
}

Error Handling

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}`);
  }
}

Platform Examples

iOS (Swift)

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)
    }
}

Android (Kotlin)

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)
        }
    }
}

React Native

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};
}

Comparison: Web SSE vs Mobile API

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

Performance

Timing (3 Notes)

Polling Recommendations


Error Codes

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

Testing

Test Interruption Handling

# 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}

Test with Mobile Browser

// 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());
}

Production Deployment

Docker & AWS

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

Scaling

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

Monitoring

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

Best Practices

  1. Implement exponential backoff for polling (2s → 3s → 5s → 10s)
  2. Show progress UI using the progress field (0-100%)
  3. Handle 404 errors gracefully (job expired)
  4. Retry on network errors (use job_id to reconnect)
  5. Cache job_id locally (allow user to resume later)
  6. Set 60-second timeout on client (but allow manual retry)
  7. Use specialty selector for better prompt matching

Advantages Over SSE for Mobile

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


FAQs

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.