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

# TypeScript SDK

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

<Info>
  **Two packages, one protocol.** There are two ways to integrate in TypeScript:

  * **`med-scribe-alliance-ts-sdk`** — the open-source [MedScribe Alliance Protocol](https://github.com/MedScribeAlliance/scribe-emr-protocol) SDK. A direct `ScribeClient` over the protocol: discovery, recording, chunked upload, session lifecycle, output retrieval. **This is the recommended integration and the main guide below.**
  * **`@eka-care/ekascribe-ts-sdk`** — an Eka Care convenience wrapper built on top of the same protocol. Adds a prebuilt recording **Widget**, **session/document utilities** (history, details, documents), and Eka `env`/`clientId` handling. Use it only if you want those extras — see [Eka Care wrapper (advanced)](#eka-care-wrapper-advanced) at the end.

  Both speak the same backend. Start with the Alliance SDK; reach for the wrapper if you need the widget or EMR session utilities.
</Info>

## 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](/api-reference/health-ai/ekascribe/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
  uploadType: 'chunked',         // 'chunked' (default) | 'single' | 'stream'
  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, {
  uploadType: 'chunked',
});
```

### 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).

***

## Eka Care Wrapper (Advanced)

<Info>
  **You only need this section if you want the wrapper-only features:** the prebuilt recording **Widget**, **session/document utilities** (history, details, documents), or Eka `env`/`clientId` handling. For everything else, the [Alliance `ScribeClient`](#integration-guide-step-by-step) above is the recommended path.
</Info>

`@eka-care/ekascribe-ts-sdk` is an Eka Care convenience wrapper over the same protocol. It exposes a singleton instance instead of `new ScribeClient()`, and the recording flow mirrors the Alliance API with slightly different method names and return shapes.

```bash theme={null}
npm install @eka-care/ekascribe-ts-sdk
# or
yarn add @eka-care/ekascribe-ts-sdk
```

[npm package →](https://www.npmjs.com/package/@eka-care/ekascribe-ts-sdk)

### Alliance → wrapper method map

| Operation       | Alliance (`ScribeClient`)                  | Eka wrapper                                                               |
| --------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
| Create client   | `new ScribeClient(config)`                 | `getEkaScribeInstance(config)` (singleton)                                |
| Start recording | `startRecording()` → `SDKResult`           | `startRecordingV2()` → `TStartRecordingResponse` (`txn_id`, `error_code`) |
| Token refresh   | `onTokenRequired` → `event.resolve(token)` | `onTokenRequired` → `return token`                                        |
| Retry uploads   | `retryFailedUploads()`                     | `retryUploadRecording()`                                                  |
| Teardown        | `reset()`                                  | `resetInstance()`                                                         |
| Get output      | `getSessionStatus(id, { poll })`           | `getSessionStatus(id, { poll })` (same)                                   |

<AccordionGroup>
  <Accordion title="Initialize the wrapper (getEkaScribeInstance)">
    The wrapper uses a **singleton pattern** — `getEkaScribeInstance()` always returns the same instance for a given `env` + `clientId` combination.

    `allianceConfig.baseUrl` is **required** — omitting it throws `[EkaScribe] 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 { getEkaScribeInstance } from '@eka-care/ekascribe-ts-sdk';
    import type { EkaScribeConfig } from '@eka-care/ekascribe-ts-sdk';

    const config: EkaScribeConfig = {
      access_token: '<your_access_token>',
      env: 'PROD',                             // 'PROD' | 'DEV'
      clientId: '<your_client_id>',            // optional
      allianceConfig: {
        baseUrl: 'https://api.eka.care/voice/v1',  // required — PROD; see table above
        useWorker: 'auto',                              // optional: true | false | 'auto'
        debug: false,                                   // optional
      },
      sharedWorkerUrl: workerUrl,              // optional
    };

    const ekascribe = getEkaScribeInstance(config);
    ```

    <Warning>
      Production APIs require a secure (HTTPS) origin — they will not work from `http://localhost`. **Recommended:** use **[ngrok](/api-reference/health-ai/ekascribe/local-development-ngrok)** to tunnel your local server over HTTPS and test against production directly. Alternatively, point at the development `baseUrl` (`https://api.dev.eka.care/voice/v1`) which works from plain `localhost`.
    </Warning>

    * Calling `getEkaScribeInstance()` again with the same config returns the same instance.
    * If `env` or `clientId` changes, the old instance is automatically reset.
    * If only `access_token` changes, the token is updated without resetting.
    * One active recording at a time. Call `endRecording()` or `cancelSession()` before starting a new one.

    ```ts theme={null}
    interface EkaScribeConfig {
      access_token?: string;          // Bearer token for authentication
      env: 'PROD' | 'DEV';           // Environment
      clientId?: string;              // Your client identifier
      mode?: 'http' | 'ipc';         // Transport mode (default: 'http')
      ipcBridge?: IpcBridge;          // Required when mode is 'ipc' (Electron apps)
      enableTracking?: boolean;       // Enable internal analytics tracking
      flavour?: string;               // Client flavour identifier
      sharedWorkerUrl?: string;       // URL to worker.bundle.js for background uploads
      allianceConfig?: {
        baseUrl?: string;             // Scribe service URL (required)
        useWorker?: boolean | 'auto'; // SharedWorker: true | false | 'auto' (default: 'auto')
        debug?: boolean;              // Enable debug logging (default: false)
      };
      widget?: WidgetConfig;          // Widget configuration — see Widget accordion
    }
    ```

    Next: register callbacks and run a recording with the wrapper methods — see the accordion below.
  </Accordion>

  <Accordion title="Complete a recording (wrapper methods)">
    A full session with the wrapper, end to end. The steps mirror the Alliance flow but use the wrapper's method names and return shapes (`startRecordingV2`, `resetInstance`, `error_code`/`txn_id` instead of `SDKResult`).

    **Step 1 — Register callbacks** (before recording). The wrapper's `onTokenRequired` **returns** the token instead of calling `event.resolve()`:

    ```ts theme={null}
    // Token refresh — wrapper calls this automatically on 401
    ekascribe.registerCallback('onTokenRequired', async () => {
      const newToken = await myAuthService.refreshToken();
      return newToken; // return the token string (10s timeout)
    });

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

    // Recording state + errors
    ekascribe.registerCallback('onRecordingStateChange', (event) => {
      console.log('State:', event.type); // 'started' | 'paused' | 'resumed' | 'ended'
    });
    ekascribe.registerCallback('onError', (event) => {
      console.error(`[${event.error.code}] ${event.error.message}`);
    });
    ```

    **Step 2 — Start recording** with `startRecordingV2()`. Returns `TStartRecordingResponse` — check `error_code`, read the session id from `txn_id`:

    ```ts theme={null}
    const result = await ekascribe.startRecordingV2({
      templates: ['clinical_notes_template'], // required: template IDs (from getConfig → my_templates)
      sessionMode: 'consultation',         // optional: 'consultation' | 'dictation'
      languageHint: ['en', 'hi'],          // optional: input audio language hints. If you're not offering users a language change option in your UI, use ['auto_detect'] for the best results.
      transcriptLanguage: 'en',            // optional: output transcript language
      model: 'pro',                        // optional: 'pro' | 'lite'
      uploadType: 'chunked',               // optional: 'chunked' (default) | 'single'
      patientDetails: { name: 'John Doe', age: '45', gender: 'male' }, // optional
    });

    if (result.error_code) {
      console.error(result.error_code, result.message);
      return;
    }

    const sessionId = result.txn_id!;
    ```

    <Note>
      Use `clinical_notes_template` for testing, or contact Eka Care to create a custom template for your use case. Call `ekascribe.sessions.getConfig()` (see the **Session & document utilities** accordion) to list the templates enabled for your account.
    </Note>

    **Step 3 — Pause / Resume** (optional, during recording):

    ```ts theme={null}
    ekascribe.pauseRecording();
    ekascribe.resumeRecording();
    ```

    **Step 4 — End recording** with `endRecording()`. On `audio_upload_failed`, retry with `retryUploadRecording()`:

    ```ts theme={null}
    const endResult = await ekascribe.endRecording();

    if (endResult.error_code === 'audio_upload_failed') {
      await ekascribe.retryUploadRecording(); // retry the failed chunks
    }
    ```

    **Step 5 — Poll for results** with `getSessionStatus()` (same shape as the Alliance SDK — returns `SDKResult`):

    ```ts theme={null}
    const status = await ekascribe.getSessionStatus(sessionId, {
      poll: {
        maxAttempts: 60,
        intervalMs: 2000,
        onProgress: (s) => console.log('Status:', s.status),
      },
    });

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

    **Step 6 — Cancel instead of end** (optional) — stops recording **without** triggering processing:

    ```ts theme={null}
    await ekascribe.cancelSession();            // current session
    await ekascribe.cancelSession('session-id'); // or by id
    ```

    **Step 7 — Clean up** — `resetInstance()` tears down the singleton (clears state, destroys the widget, removes callbacks). Call `getEkaScribeInstance()` again afterwards:

    ```ts theme={null}
    await ekascribe.resetInstance();
    ```

    #### Full example

    ```ts theme={null}
    import {
      getEkaScribeInstance,
      type EkaScribeConfig,
    } from '@eka-care/ekascribe-ts-sdk';

    // 1. Initialize
    const ekascribe = getEkaScribeInstance({
      access_token: token,
      env: 'PROD',
      allianceConfig: { baseUrl: 'https://api.eka.care/voice/v1' },
    });

    // 2. Callbacks
    ekascribe.registerCallback('onTokenRequired', async () => await refreshToken());
    ekascribe.registerCallback('onError', (e) => showErrorToast(e.error.message));

    // 3. Start
    const start = await ekascribe.startRecordingV2({
      templates: ['clinical_notes_template'],
      sessionMode: 'consultation',
      languageHint: ['en'],
    });
    if (start.error_code) return showError(start.message);
    const sessionId = start.txn_id!;

    // 4. ...user records (pause/resume optional)...

    // 5. End
    const end = await ekascribe.endRecording();
    if (end.error_code === 'audio_upload_failed') {
      await ekascribe.retryUploadRecording();
    }

    // 6. Results
    const status = await ekascribe.getSessionStatus(sessionId, {
      poll: { maxAttempts: 60, intervalMs: 2000 },
    });
    if (status.success) displayResults(status.data.templates, status.data.transcript);

    // 7. Cleanup on unmount
    await ekascribe.resetInstance();
    ```
  </Accordion>

  <Accordion title="Bundler setup (Vite / Webpack / Next.js)">
    The wrapper uses a SharedWorker for background audio uploads. Modern bundlers handle this automatically.

    **Vite** — works out of the box.
    **Webpack 5** — works out of the box (`new URL(..., import.meta.url)` is natively supported).

    **Next.js** — ensure the SDK is used only on the client:

    ```tsx theme={null}
    'use client';

    import { getEkaScribeInstance } from '@eka-care/ekascribe-ts-sdk';
    ```

    **Browser (script tag):**

    ```html theme={null}
    <script type="module">
      import { getEkaScribeInstance } from 'https://cdn.jsdelivr.net/npm/@eka-care/ekascribe-ts-sdk/dist/index.mjs';
    </script>
    ```

    **SharedWorker URL** — pass `sharedWorkerUrl` in config. Resolve it with the wrapper's helper:

    ```ts theme={null}
    import { createWorkerBlobUrl } from '@eka-care/ekascribe-ts-sdk';

    const workerUrl = await createWorkerBlobUrl();
    const ekascribe = getEkaScribeInstance({ /* ... */ sharedWorkerUrl: workerUrl });
    // Remember to URL.revokeObjectURL(workerUrl) when done.
    ```
  </Accordion>

  <Accordion title="Prebuilt Widget (zero-UI recording)">
    The wrapper provides an optional pre-built recording UI injected via Shadow DOM — you write zero UI code.

    **Step 1:** Enable the widget in config with session defaults and callbacks:

    ```ts theme={null}
    const ekascribe = getEkaScribeInstance({
      access_token: token,
      env: 'PROD',
      allianceConfig: { baseUrl: '...' },
      widget: {
        enabled: true,
        orientation: 'horizontal',          // 'horizontal' | 'vertical'
        zIndex: 9999,                       // optional
        position: { bottom: 20, right: 20 }, // optional
        sessionDefaults: {
          input_language: ['en'],
          output_format_template: [{ template_id: 'clinical_notes_template' }],
          model_type: 'pro',
          mode: 'consultation',
        },
        callbacks: {
          onRecordingStart: ({ txn_id }) => {},
          onRecordingStop: ({ txn_id, duration }) => {},
          onProcessingComplete: ({ txn_id, sessionData }) => {
            // sessionData contains templates, transcript, etc.
          },
          onError: ({ error_code, message }) => {},
        },
      },
    });
    ```

    **Step 2:** Call `startForPatient()` per patient — the widget appears and the user drives it (pause, resume, stop). Results arrive via callbacks.

    ```ts theme={null}
    await ekascribe.startForPatient({
      txn_id: 'unique-session-id',
      patient_details: {                  // optional
        username: 'John Doe',
        age: 45,
        biologicalSex: 'M',
      },
      additional_data: {},                // optional
    });
    ```

    The widget handles `startRecordingV2()`, `pauseRecording()`, `resumeRecording()`, `endRecording()`, and `getSessionStatus()` internally.

    **Widget state flow:**

    ```
    COLLAPSED ──> RECORDING ──> PAUSED ──> RECORDING ──> PROCESSING ──> DONE
         ^             │                                       │           │
         │             └──── (user clicks stop) ───────────────┘           │
         │                                                                 │
         └──────────── (user clicks close) ────────────────────────────────┘
                                                       │
                                                   ERROR
    ```

    ```ts theme={null}
    interface WidgetConfig {
      enabled: boolean;
      theme?: 'light' | 'dark';
      zIndex?: number;
      primaryColor?: string;
      position?: { bottom?: number; right?: number; top?: number; left?: number };
      orientation?: 'horizontal' | 'vertical';
      callbacks?: WidgetCallbacks;
      sessionDefaults: {
        input_language: string[];
        output_format_template: { template_id: string; template_name?: string; template_type?: string }[];
        model_type: string;
        mode: string;
      };
    }

    interface StartForPatientConfig {
      txn_id: string;
      patient_details?: {
        username?: string;
        age?: number;
        biologicalSex?: string;
        mobile?: string;
      };
      additional_data?: Record<string, unknown>;
    }

    interface WidgetCallbacks {
      onRecordingStart?: (data: { txn_id: string }) => void;
      onRecordingPause?: (data: { txn_id: string; duration: number }) => void;
      onRecordingResume?: (data: { txn_id: string }) => void;
      onRecordingStop?: (data: { txn_id: string; duration: number }) => void;
      onProcessingStart?: (data: { txn_id: string }) => void;
      onProcessingComplete?: (data: { txn_id: string; sessionData: unknown }) => void;
      onError?: (data: { error_code: string; message: string }) => void;
      onWidgetClose?: (data: { txn_id: string }) => void;
    }
    ```
  </Accordion>

  <Accordion title="Session & document utilities">
    Wrapper-only helpers for fetching session history, details, and documents.

    **`getSessionHistory(request)`** — fetch previous sessions.

    ```ts theme={null}
    const sessions = await ekascribe.sessions.getSessionHistory({
      txn_count: 10,        // number of sessions to fetch
      oid: 'patient-oid',   // optional: filter by patient oid
    });
    ```

    **`getSessionDetails(request)`** — detailed info including documents, context, and presigned URLs.

    ```ts theme={null}
    const details = await ekascribe.sessions.getSessionDetails({
      session_id: 'session-id',
      presigned: true,         // include presigned URLs for documents
    });

    const doc = details.data?.documents[0];
    if (doc?.presigned_url) {
      const response = await fetch(doc.presigned_url);
      const content = await response.json();
    }
    ```

    > Presigned URLs are temporary — check `presigned_url_expires_at` (epoch) before using. Call `getDocument(documentId)` for a fresh URL if expired.

    **`getDocument(documentId)`** — fetch a single document by ID (and a fresh presigned URL).

    ```ts theme={null}
    const doc = await ekascribe.documents.getDocument('document-id');
    if (doc.data?.presigned_url) {
      const response = await fetch(doc.presigned_url);
      const content = await response.json();
    }
    ```

    **`patchSessionStatus(request, sessionId?)`** — update session properties.

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

    **Config** — call `ekascribe.sessions.getConfig()` to fetch your account config: supported languages, output formats, consultation modes, and `my_templates` (the valid `template_id` values to pass when starting a recording). `clinical_notes_template` is a ready-to-use testing template; contact Eka Care for a custom one.

    ```ts theme={null}
    const config = await ekascribe.sessions.getConfig();
    config.data?.my_templates.forEach((t) => console.log(t.id, t.name));
    ```

    Response: `TGetConfigV2Response`

    ```ts theme={null}
    type TGetConfigV2Response = {
      data?: {
        supported_languages: TGetConfigItem[];
        supported_output_formats: TGetConfigItem[];
        consultation_modes: TGetConfigItem[];
        max_selection: {
          supported_languages: number;
          supported_output_formats: number;
          consultation_modes: number;
        };
        settings: TConfigSettings;
        my_templates: { id: string; name: string }[];   // valid template_id values for your account
        user_details: {
          uuid: string;
          fn: string;
          mn: string;
          ln: string;
          dob: string;
          gen: 'F' | 'M' | 'O';
          s: string;
          'w-id': string;
          'w-n': string;
          'b-id': string;
          is_paid_doc: boolean;
          is_eka_doc: boolean;
          oid: string;
        };
        selected_preferences?: TSelectedPreferences;
        clinic_name?: string;
        specialization?: string;
        emr_name?: string;
        microphone_permission_check?: boolean;
        consult_language?: string[];
        contact_number?: string;
        onboarding_step?: string;
        header?: TConfigHeaderFooter;
        footer?: TConfigHeaderFooter;
      };
      message?: string;
      status_code: number;
    };
    ```

    **Discovery** — `ekascribe.sessions.getDiscoveryDocument()` mirrors the Alliance discovery method.
  </Accordion>

  <Accordion title="Pre-recorded audio upload">
    Upload a pre-recorded audio file instead of live recording, for non-real-time flows.

    1. Create session via `ekascribe.sessions.createSession()`
    2. Upload audio via `processPreRecordedAudio()`
    3. End session via `ekascribe.sessions.endSession()`

    ```ts theme={null}
    const result = await ekascribe.processPreRecordedAudio({
      uploadUrl: session.upload_url,       // from createSession response
      audioFile: audioBlob,                // File or Blob
    });
    ```
  </Accordion>

  <Accordion title="Authentication (updateAuthTokens)">
    Manually update the access token — propagates to all internal transports and the worker.

    ```ts theme={null}
    ekascribe.updateAuthTokens({ access_token: 'new-token' });
    ```

    > If you have `onTokenRequired` registered, the wrapper handles 401s automatically. You only need `updateAuthTokens()` for proactive token rotation (e.g., before expiry).
  </Accordion>

  <Accordion title="Wrapper error codes">
    The wrapper's `TStartRecordingResponse` / `TEndRecordingResponse` carry an `error_code` field (unlike the Alliance `SDKResult` error classes).

    | Error Code              | Description                                                |
    | ----------------------- | ---------------------------------------------------------- |
    | `microphone`            | Microphone access error (permission denied or unavailable) |
    | `txn_init_failed`       | Failed to initialize session                               |
    | `txn_limit_exceeded`    | Maximum concurrent sessions exceeded                       |
    | `internal_server_error` | Unexpected server-side error                               |
    | `end_recording_failed`  | Failed to end recording                                    |
    | `audio_upload_failed`   | Audio file upload to server failed                         |
    | `txn_commit_failed`     | Commit call failed                                         |
    | `txn_status_mismatch`   | Invalid operation for current session state                |
    | `network_error`         | Network connectivity issue                                 |
    | `unknown_error`         | Unclassified error                                         |
    | `unauthorized`          | Authentication failed (invalid or expired token)           |
    | `forbidden`             | Insufficient permissions                                   |
  </Accordion>

  <Accordion title="Deprecated wrapper methods">
    These methods are from older wrapper versions. They still work but are not recommended for new integrations.

    | Deprecated Method                        | Use Instead                               |
    | ---------------------------------------- | ----------------------------------------- |
    | `initTransaction()` + `startRecording()` | `startRecordingV2()`                      |
    | `getTemplateOutput()`                    | `getSessionStatus()` with polling         |
    | `getOutputTranscription()`               | `getSessionStatus()` with polling         |
    | `commitTransactionCall()`                | Handled automatically by `endRecording()` |
    | `stopTransactionCall()`                  | Handled automatically by `endRecording()` |
  </Accordion>
</AccordionGroup>

***

Refer to the [MedScribe Alliance Protocol](https://github.com/MedScribeAlliance/scribe-emr-protocol), the [MedScribe Alliance TS SDK](https://github.com/eka-care/medScribeAlliance-ts-sdk), and the [EkaScribe TS SDK](https://github.com/eka-care/eka-js-sdk) repositories for implementation details.
