> ## Documentation Index
> Fetch the complete documentation index at: https://developer.eka.care/llms.txt
> Use this file to discover all available pages before exploring further.

# MedScribe Alliance SDK (TypeScript)

> Record in the browser and get structured medical notes back — the open MedScribe Alliance Protocol SDK for TypeScript.

Capture and process audio in the browser and generate structured medical documentation through Eka Care's voice transcription service.

`med-scribe-alliance-ts-sdk` is the open-source [MedScribe Alliance SDK](https://github.com/eka-care/medScribeAlliance-ts-sdk). It gives you a `ScribeClient` that speaks the MedScribe Alliance protocol directly: discovery, recording, chunked upload, session lifecycle, output retrieval.

## Prerequisites

* Node 14+
* `npm` or `yarn`
* Microphone access via browser permissions
* Stable network connectivity
* An access token from Eka Care

## Installation

```bash theme={null}
npm install med-scribe-alliance-ts-sdk
```

Peer dependencies (installed automatically):

* `@ricky0123/vad-web` — Voice Activity Detection
* `@breezystack/lamejs` — MP3 encoding
* `zod` — Schema validation

[npm package →](https://www.npmjs.com/package/med-scribe-alliance-ts-sdk)

***

## Integration Guide (Step-by-Step)

### Step 1: Create the Client

The `baseUrl` is **required** — every API call (session creation, upload, status polling) goes through it. If you leave it out, the SDK throws `allianceConfig.baseUrl is required` at runtime.

To use Eka Care's hosted scribe service, use:

| Environment | `baseUrl` |
| - | - |
| Production | `https://api.eka.care/voice/v1` |
| Development | `https://api.dev.eka.care/voice/v1` |

```ts theme={null}
import { ScribeClient } from 'med-scribe-alliance-ts-sdk';

const client = new ScribeClient({
  baseUrl: 'https://api.eka.care/voice/v1', // PROD — see table above
  accessToken: 'your-bearer-token',
  debug: true, // optional: logs SDK activity to console
});
```

<Warning>
  **Production APIs require a secure (HTTPS) origin.** They will not work from `http://` or `http://localhost` — an insecure origin fails the CORS preflight (the `authorization` header is rejected).

  **Recommended:** Use **[ngrok](/ekascribe/resources/local-development-ngrok)** to give your local server a public HTTPS URL and test against the production `baseUrl` directly.

  Alternatively, point at the **staging `baseUrl`** (`https://api.dev.eka.care/voice/v1`) which works from plain `localhost`.
</Warning>

### Step 2: Initialize (Discovery)

`init()` fetches the discovery document from the server. This tells the SDK what the server supports (models, languages, upload methods, audio formats, etc.).

```ts theme={null}
const initResult = await client.init();
if (!initResult.success) {
  console.error('Init failed:', initResult.error.message);
  return;
}
```

> `startRecording()` calls `init()` automatically if not already initialized. You can skip this step if you go directly to recording.

### Step 3: Register Callbacks

Register callbacks **before** starting a recording. These are how you receive events from the SDK.

```ts theme={null}
// Upload progress
client.registerCallback('onUploadEvent', (event) => {
  if (event.type === 'progress') {
    console.log(`Uploaded ${event.data.successCount}/${event.data.totalCount}`);
  }
});

// Recording state changes
client.registerCallback('onRecordingStateChange', (event) => {
  console.log('Recording state:', event.type); // 'started' | 'paused' | 'resumed' | 'ended'
});

// Errors (VAD failures, network issues, validation)
client.registerCallback('onError', (event) => {
  console.error(`[${event.error.code}] ${event.error.message}`);
});

// Auto token refresh on 401
client.registerCallback('onTokenRequired', async (event) => {
  const newToken = await refreshMyAuthToken();
  event.resolve(newToken);
});
```

### Step 4: Start Recording

Creates a session, starts the microphone, and begins chunked upload in one call.

```ts theme={null}
const result = await client.startRecording({
  templates: ['clinical_notes_template'], // required: template IDs for extraction
  sessionMode: 'consultation',   // optional: 'consultation' | 'dictation'
  transcriptLanguage: 'en',      // optional: language code for transcript output
  languageHint: ['en', 'hi'],    // optional: language codes for audio input. If you're not offering users a language change option in your UI, use ['auto_detect'] for the best results.
  patientDetails: {              // optional
    name: 'John Doe',
    age: '45',
    gender: 'male',
  },
  additionalData: {},            // optional: any extra data for the session
  txnId: 'your-transaction-id',  // optional: external transaction ID
});

if (!result.success) {
  console.error('Failed to start:', result.error.message);
  return;
}

const sessionId = result.data.session_id;
```

<Note>
  Use `clinical_notes_template` for testing, or contact Eka Care to create a custom template for your use case.
</Note>

#### Pause / Resume

```ts theme={null}
client.pauseRecording();  // pauses VAD — mic stays open, no new chunks created
client.resumeRecording(); // resumes VAD processing
```

### Step 5: End Recording

Stops the microphone, flushes the last audio chunk, waits for all uploads to complete, and tells the server the session has ended (triggers server-side processing).

```ts theme={null}
const stopResult = await client.endRecording();

if (stopResult.success) {
  console.log(`${stopResult.data.totalFiles} files uploaded`);
  console.log(`${stopResult.data.failedUploads.length} failed`);
}
```

### Step 6: Poll for Results

After ending the recording, poll the server until processing is complete.

```ts theme={null}
const abortController = new AbortController();

const status = await client.getSessionStatus(sessionId, {
  poll: {
    maxAttempts: 60,
    intervalMs: 2000,
    signal: abortController.signal, // optional: abort polling early
    onProgress: (s) => {
      console.log(`Status: ${s.status}`);
      if (s.templates) {
        console.log('Templates:', s.templates);
      }
    },
  },
});

if (status.success) {
  console.log('Final status:', status.data.status);
  console.log('Templates:', status.data.templates);
  console.log('Transcript:', status.data.transcript);
}
```

### Step 7: Clean Up

```ts theme={null}
await client.reset(); // stops recording if active, clears all state and caches
```

### Flow Diagram

```
  new ScribeClient({ baseUrl, accessToken })
         │
         ▼
      init()  ──────────  Fetches discovery (auto-called by startRecording)
         │
         ▼
  registerCallback()  ──  Set up event handlers before recording
         │
         ▼
  startRecording()  ────  Creates session → starts mic → begins upload
         │
    pause / resume  ────  Optional during recording
         │
         ▼
  endRecording()  ──────  Stops mic → flushes audio → ends session → triggers processing
         │
         ▼
  getSessionStatus()  ──  Poll until completed/failed
         │
         ▼
  Read results  ────────  templates, transcript, errors
```

***

## Important Notes

* **`baseUrl` is the root for all API calls.** Session creation, audio upload, status polling — everything uses this URL. Make sure it's correct and accessible.
* **`accessToken` must be a valid Bearer token.** All API requests include `Authorization: Bearer <token>`. If it expires, register `onTokenRequired` to auto-refresh.
* **Register callbacks before `startRecording()`.** Events fire immediately once recording starts — if callbacks aren't registered, you'll miss upload progress and errors.
* **`endRecording()` triggers server processing.** Once you call it, the server begins processing the uploaded audio. Use `cancelSession()` instead if you don't want processing to happen.
* **`cancelSession()` does NOT trigger processing.** It stops the recorder locally, cleans up state, and tells the server the session is cancelled. No `endSession` call is made to the backend.
* **All async methods return `SDKResult<T>`, never throw.** Always check `result.success` before accessing `result.data`. Errors are in `result.error`.
* **The SDK validates inputs against the discovery document.** If the server doesn't support an upload type, language, or model you requested, you'll get a `ValidationError` before the API call is made.
* **SharedWorker is optional.** If you provide `workerScriptUrl`, the SDK offloads MP3 compression and upload to a SharedWorker. If the worker fails to load, it silently falls back to main-thread processing.
* **Microphone permission is requested on `startRecording()`.** The browser will prompt the user for mic access. If denied, you'll get an error via `onError` callback.
* **`reset()` is a full teardown.** It destroys the transport, clears discovery cache, removes all callbacks, and sets the client back to uninitialized state. You'll need to call `init()` (or `startRecording()`) again after reset.
* **Polling supports `AbortSignal`.** Pass `signal` in poll options to cancel polling early (e.g. when the user navigates away).

***

## Other Operations

### Cancel a Session

Stops the recorder locally **without** triggering server-side processing, then tells the server the session is cancelled.

```ts theme={null}
await client.cancelSession(); // cancels the current active session
await client.cancelSession('specific-session-id'); // or by ID
```

### Update a Session (Patch)

Update session properties after creation.

```ts theme={null}
await client.updateSession({
  patient_details: { name: 'Jane Doe', age: '30', gender: 'female' },
  additional_data: { notes: 'Follow-up visit' },
  templates: ['soap', 'prescription'],
});
```

### Two-Step Flow (Create Session + Record Separately)

```ts theme={null}
// Step 1: Create session
const session = await client.createSession({
  templates: ['soap'],
  upload_type: 'chunked',
  communication_protocol: 'http',
  session_mode: 'consultation',
});

if (!session.success) return;

// Step 2: Start recording with the existing session
await client.startRecordingWithSession(session.data);
```

### Get Status for a Specific Template

```ts theme={null}
const status = await client.getSessionStatus(sessionId, {
  templateId: 'soap',
});
```

### Retry Failed Uploads

```ts theme={null}
if (client.hasFailedUploads()) {
  const retryResult = await client.retryFailedUploads();
  console.log(`Retried: ${retryResult.data.retried}, Succeeded: ${retryResult.data.succeeded}`);
}
```

### Update Auth Token

```ts theme={null}
client.setAccessToken('new-bearer-token');
```

***

## Configuration

```ts theme={null}
interface ScribeSDKConfig {
  /** Base URL of the scribe service (required) */
  baseUrl: string;

  /** Bearer token for authentication */
  accessToken?: string;

  /** Transport mode: 'direct' (HTTP) or 'ipc' (Electron). Default: 'direct' */
  mode?: 'direct' | 'ipc';

  /** IPC bridge — required when mode is 'ipc' */
  ipcTransport?: IpcBridge;

  /** SharedWorker: true (require), false (disable), 'auto' (detect). Default: 'auto' */
  useWorker?: boolean | 'auto';

  /** URL to worker.bundle.js. Use getWorkerUrl() to resolve. */
  workerScriptUrl?: string;

  /** Enable debug logging. Default: false */
  debug?: boolean;

  /** Auto-fetch discovery document on init. Default: true */
  autoDiscovery?: boolean;
}
```

## Recording Options

```ts theme={null}
interface RecordingOptions {
  templates: string[];                   // Template IDs for extraction (required)
  model?: string;                        // Model ID from discovery
  languageHint?: string[];               // Language codes for audio input
  transcriptLanguage?: string;           // Language code for transcript output
  uploadType?: string;                   // 'chunked' | 'single' | 'stream' (default: 'chunked')
  communicationProtocol?: string;        // 'http' | 'websocket' (default: 'http')
  additionalData?: Record<string, any>;  // Extra data for the session
  deviceId?: string;                     // Specific microphone device ID
  sessionMode?: string;                  // 'consultation' | 'dictation'
  patientDetails?: PatientDetails;       // Patient info
  txnId?: string;                        // External transaction ID
}
```

## API Reference

### Lifecycle

| Method | Returns | Description |
| - | - | - |
| `init()` | `SDKResult<void>` | Fetch discovery document. Called automatically by `startRecording`. |
| `reset()` | `Promise<void>` | Stop recording, clear all state and caches. |

### Recording

| Method | Returns | Description |
| - | - | - |
| `startRecording(options)` | `SDKResult<CreateSessionResponse>` | Create session + start mic + begin upload. |
| `startRecordingWithSession(session, options?)` | `SDKResult<void>` | Attach recorder to an existing session. |
| `pauseRecording()` | `void` | Pause VAD (mic stays open, no chunks created). |
| `resumeRecording()` | `void` | Resume VAD processing. |
| `endRecording()` | `SDKResult<StopRecordingResult>` | Stop mic, flush audio, wait for uploads, end session. |
| `isRecording()` | `boolean` | Whether a recording is active. |
| `isRecordingPaused()` | `boolean` | Whether the active recording is paused. |
| `retryFailedUploads()` | `SDKResult<RetryUploadResult>` | Retry uploads that failed during the last recording. |
| `hasFailedUploads()` | `boolean` | Whether there are retryable failed uploads. |

### Session

| Method | Returns | Description |
| - | - | - |
| `createSession(request)` | `SDKResult<CreateSessionResponse>` | Create a session without starting a recording. |
| `getSessionStatus(sessionId?, options?)` | `SDKResult<GetSessionStatusResponse>` | Get status. Supports `poll` and `templateId` options. |
| `getCurrentSession()` | `CreateSessionResponse \| null` | Get the active session if any. |
| `updateSession(request, sessionId?)` | `SDKResult<PatchSessionResponse>` | Patch session (patient details, status, etc.). |
| `cancelSession(sessionId?)` | `SDKResult<PatchSessionResponse>` | Cancel session (stops recorder, no server processing). |

### Discovery

| Method | Returns | Description |
| - | - | - |
| `getDiscoveryDocument()` | `DiscoveryDocument \| null` | Raw discovery document. |
| `getDiscoveryConfig()` | `SDKResult<ResolvedConfig>` | Resolved config from discovery. |
| `refreshDiscovery()` | `SDKResult<ResolvedConfig>` | Force-refresh discovery. |

### Auth

| Method | Description |
| - | - |
| `setAccessToken(token)` | Update Bearer token. Propagates to transport, recorder, and worker. |

### Callbacks

Register with `client.registerCallback(name, handler)`, remove with `client.removeCallback(name, handler)`.

| Callback | Payload | Description |
| - | - | - |
| `onRecordingStateChange` | `RecordingStateChangeEvent` | Recording started, paused, resumed, or ended. |
| `onAudioEvent` | `AudioEvent` | Speech detection, silence warnings, chunk ready. |
| `onUploadEvent` | `UploadEvent` | Upload progress and failures. |
| `onSessionEvent` | `SessionEvent` | Session created, ended, status updates. |
| `onError` | `ErrorEvent` | VAD, worker, transport, or validation errors. |
| `onTokenRequired` | `TokenRequiredEvent` | 401 received — call `event.resolve(newToken)` to retry. |

#### Payload Shapes

```ts theme={null}
// onRecordingStateChange
interface RecordingStateChangeEvent {
  type: 'started' | 'paused' | 'resumed' | 'ended';
  timestamp: string;
  data?: any;
}

// onAudioEvent — discriminated union by `type`
type AudioEvent =
  | { type: 'user_speech';      timestamp: string; data: { isSpeaking: boolean } }
  | { type: 'silence_warning';  timestamp: string; data: { durationMs: number } }
  | { type: 'chunk_ready';      timestamp: string; data: { chunkIndex: number; fileName: string; chunkData: Uint8Array[] } }
  | { type: 'frame_processed';  timestamp: string; data: { isSpeech: number; notSpeech: number; frame: Float32Array; duration: number } };

// onUploadEvent
type UploadEvent =
  | { type: 'progress'; timestamp: string; data: { successCount: number; totalCount: number } }
  | { type: 'failed';   timestamp: string; data: { fileName: string; error: string } }
  | { type: 'retry';    timestamp: string; data: { fileName: string; attempt: number } };

// onSessionEvent
type SessionEvent =
  | { type: 'created';        timestamp: string; data: CreateSessionResponse }
  | { type: 'ended';          timestamp: string; data: EndSessionResponse }
  | { type: 'discarded';      timestamp: string; data: { sessionId: string | null; reason: 'cleared' | 'cancelled' | 'reset' } }
  | { type: 'status_update';  timestamp: string; data: GetSessionStatusResponse }
  | { type: 'partial_result'; timestamp: string; data: any };

// onError
interface ErrorEvent {
  type: 'vad_error' | 'worker_error' | 'transport_error' | 'validation_error';
  timestamp: string;
  error: { code: string; message: string; details?: any };
}

// onTokenRequired — call event.resolve(newToken) to retry the failed request
interface TokenRequiredEvent {
  resolve: (newToken: string) => void;
}
```

## Request / Response Types

#### Session

```ts theme={null}
interface CreateSessionRequest {
  templates: string[];
  upload_type: string;                   // 'chunked' | 'single' | 'stream'
  communication_protocol: string;        // 'http' | 'websocket'
  model?: string;
  language_hint?: string[];
  transcript_language?: string;
  additional_data?: Record<string, any>;
  session_mode?: string;                 // 'consultation' | 'dictation'
  patient_details?: PatientDetails;
  session_id?: string;                   // optional client-supplied ID
}

interface CreateSessionResponse {
  session_id: string;
  status: SessionStatus;
  created_at: string;
  expires_at: string;
  upload_url: string;
  patient_details?: PatientDetails;
}

interface PatchSessionRequest {
  user_status?: string;
  processing_status?: string;
  patient_details?: PatientDetails;
  additional_data?: Record<string, any>;
  language_hint?: string[];
  transcript_language?: string;
  templates?: string[];
}

interface PatchSessionResponse {
  session_id: string;
  status: string;
  message: string;
}

interface EndSessionResponse {
  session_id: string;
  status: SessionStatus;
  message: string;
  audio_files_received: number;
  audio_files: string[];
}

interface GetSessionStatusResponse {
  session_id: string;
  status: SessionStatus;
  created_at: string;
  expires_at?: string | null;
  expired_at?: string | null;
  completed_at?: string | null;
  model_used?: string | null;
  language_detected?: string | null;
  audio_files_received: number;
  audio_files: string[];
  audio_files_processed?: number;
  additional_data: Record<string, any>;
  templates?: TemplateEntry[];           // { [templateId]: { status, data, fhir, error, ... } }
  transcript?: string;
  processing_errors?: ProcessingError[];
  error?: { code: string; message: string; details?: Record<string, any> };
  patient_details?: PatientDetails;
  message?: string;
}

interface ProcessTemplateResponse {
  session_id: string;
  template_id: string;
  status: string;
  message: string;
}

interface PatientDetails {
  oid?: string;
  name?: string;
  age?: string;
  gender?: string;
  mobile?: number;
}
```

#### Recording

```ts theme={null}
interface StopRecordingResult {
  failedUploads: string[];
  totalFiles: number;
}

interface EndRecordingResult extends StopRecordingResult {
  sessionEnded: boolean;
  endSessionResponse?: EndSessionResponse;
}

interface RetryUploadResult {
  retried: number;
  succeeded: number;
  stillFailed: string[];
}

interface PollOptions {
  maxAttempts?: number;
  intervalMs?: number;
  onProgress?: (status: GetSessionStatusResponse) => void;
  signal?: AbortSignal;
}
```

## Error Handling

All public async methods return `SDKResult<T>` — errors are returned, not thrown:

```ts theme={null}
type SDKResult<T> =
  | { success: true; data: T }
  | { success: false; error: ScribeError };
```

```ts theme={null}
const result = await client.startRecording({ templates: ['soap'] });

if (!result.success) {
  console.error(result.error.code, result.error.message);
  return;
}

// result.data is typed as CreateSessionResponse
console.log(result.data.session_id);
```

### Error Classes

| Error | HTTP | Description |
| - | - | - |
| `ScribeError` | — | Base error class |
| `ValidationError` | 400 | Invalid request or config |
| `AuthenticationError` | 401 | Auth failed (after token refresh attempt) |
| `ForbiddenError` | 403 | Access denied |
| `SessionNotFoundError` | 404 | Session doesn't exist |
| `SessionExpiredError` | 410 | Session expired |
| `RateLimitError` | 429 | Rate limit exceeded |
| `DiscoveryError` | — | Discovery fetch/parse failed |
| `TransportError` | — | Network / IPC failure |
| `WorkerError` | — | SharedWorker failure |
| `UploadError` | — | Audio upload failure |

## SharedWorker Support

The SDK offloads MP3 compression and upload to a SharedWorker for better main-thread performance. The worker is bundled separately as `dist/worker.bundle.js`.

### Setup

```ts theme={null}
import { ScribeClient, getWorkerUrl } from 'med-scribe-alliance-ts-sdk';

const client = new ScribeClient({
  baseUrl: 'https://api.example.com',
  workerScriptUrl: getWorkerUrl(), // or a custom path
});
```

### Serving the Worker

The worker file must be served as a static asset:

**Copy to your public directory:**

```bash theme={null}
cp node_modules/med-scribe-alliance-ts-sdk/dist/worker.bundle.js public/
```

**Or use a CDN blob URL (avoids same-origin restrictions):**

```ts theme={null}
import { createWorkerBlobUrl } from 'med-scribe-alliance-ts-sdk';

const workerUrl = await createWorkerBlobUrl();
const client = new ScribeClient({
  baseUrl: '...',
  workerScriptUrl: workerUrl,
});
```

**Or set a global override:**

```ts theme={null}
window.__MEDSCRIBE_WORKER_URL__ = '/assets/worker.bundle.js';
```

If the SharedWorker fails to initialize, the SDK silently falls back to main-thread compression and upload.

## Electron / IPC Mode

For Electron apps where network requests must go through the main process:

```ts theme={null}
import { ScribeClient, TransportMode } from 'med-scribe-alliance-ts-sdk';

const client = new ScribeClient({
  baseUrl: 'https://api.example.com',
  mode: TransportMode.IPC,
  ipcTransport: {
    send: (request) => ipcRenderer.send('scribe-request', request),
    onResponse: (handler) => ipcRenderer.on('scribe-response', (_, res) => handler(res)),
  },
});
```

IPC mode always uses main-thread compression (SharedWorker can't access the IPC bridge).

***

## Source and specification

* [MedScribe Alliance Protocol](https://github.com/MedScribeAlliance/scribe-emr-protocol) — the open protocol this SDK implements
* [MedScribe Alliance TS SDK](https://github.com/eka-care/medScribeAlliance-ts-sdk) — source for `med-scribe-alliance-ts-sdk`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.