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

# EkaScribe TypeScript SDK

> Record consultations in the browser and get structured medical notes back, with a prebuilt widget and session/document utilities.

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

`@eka-care/ekascribe-ts-sdk` handles microphone capture, voice-activity detection, chunked upload and session lifecycle for you. It also ships a prebuilt recording **Widget**, **session/document utilities** (history, details, documents), and Eka `env` / `clientId` handling.

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

***

## Integration Guide (Step-by-Step)

### Step 1: Initialize the SDK

The SDK uses a **singleton pattern** — `getEkaScribeInstance()` always returns the same instance for a given `env` + `clientId` combination.

`flavour` and `allianceConfig.baseUrl` are both **required**. `flavour` identifies your app variant on every request — Eka Care issues the value to use. Omitting `baseUrl` throws `[EkaScribe] allianceConfig.baseUrl is required` at runtime.

To use Eka Care's hosted scribe service, set `baseUrl` to:

| 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'
  flavour: '<your_client_identifier>',               // required — app variant identifier
  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://` or `http://localhost` — an insecure origin fails the CORS preflight (the `authorization` header is rejected).

  **Recommended:** use **[ngrok](/ekascribe/resources/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>

**Singleton behaviour:**

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

### Step 2: Register Callbacks

Register callbacks **before** starting a recording — events fire immediately once recording starts.

<Note>
  `onTokenRequired` must **return** the new token as a string. The handler has a **10 second** timeout.
</Note>

```ts theme={null}
// Token refresh — called 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}`);
});
```

See [Callbacks](#callbacks) for the full list and payload shapes.

### Step 3: Start Recording

`startRecordingV2()` creates the session, starts the microphone, and begins chunked upload in one call. It returns `TStartRecordingResponse` — check `error_code`, then 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'
  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()`](#account-config-getconfig) to list the templates enabled for your account.
</Note>

### Step 4: Pause / Resume

Optional, during recording. Pausing stops VAD processing — the mic stays open and no new chunks are created.

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

### Step 5: End Recording

Stops the microphone, flushes the last audio chunk, waits for uploads, and tells the server the session has ended — which **triggers server-side processing**. On `audio_upload_failed`, retry the failed chunks 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 6: Poll for Results

`getSessionStatus()` returns an `SDKResult` — check `success` before reading `data`.

```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 7: 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 8: Clean Up

`resetInstance()` tears down the singleton — clears state, destroys the widget, removes callbacks. Call `getEkaScribeInstance()` again afterwards to get a fresh instance.

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

### Flow Diagram

```
  getEkaScribeInstance({ env, access_token, allianceConfig })
         │
         ▼
  registerCallback()  ────  Set up event handlers before recording
         │
         ▼
  startRecordingV2()  ────  Creates session → starts mic → begins upload
         │                  Returns { txn_id, error_code }
    pause / resume  ──────  Optional during recording
         │
         ├─── cancelSession()  ──  Stop WITHOUT processing
         ▼
  endRecording()  ────────  Stops mic → flushes audio → ends session → triggers processing
         │                  On 'audio_upload_failed' → retryUploadRecording()
         ▼
  getSessionStatus()  ────  Poll until completed/failed
         │
         ▼
  resetInstance()  ───────  Full teardown
```

### 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',
  flavour: '<your_client_identifier>',
  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();
```

***

## Important Notes

* **`flavour` is required.** It identifies your app variant on every request. Eka Care issues the value to use.
* **`allianceConfig.baseUrl` is the root for all API calls.** Session creation, audio upload, status polling — everything uses this URL. It is required; omitting it throws at runtime.
* **`access_token` must be a valid Bearer token.** All API requests include `Authorization: Bearer <token>`. If it expires, register `onTokenRequired` to auto-refresh.
* **Register callbacks before `startRecordingV2()`.** Events fire immediately once recording starts — if callbacks aren't registered, you'll miss upload progress and errors.
* **`endRecording()` triggers server processing.** Use `cancelSession()` instead if you don't want processing to happen.
* **Recording methods and status methods report errors differently.** `startRecordingV2()` / `endRecording()` return an `error_code` field; `getSessionStatus()` returns an `SDKResult` you check with `result.success`.
* **One active recording at a time.** Call `endRecording()` or `cancelSession()` before starting a new one.
* **Microphone permission is requested on `startRecordingV2()`.** If denied, you get the `microphone` error code.
* **`resetInstance()` is a full teardown.** It clears state, destroys the widget and removes callbacks. Call `getEkaScribeInstance()` again afterwards.

***

## Bundler Setup (Vite / Webpack / Next.js)

The SDK 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 built-in 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.
```

***

## Prebuilt Widget (zero-UI recording)

The SDK 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',
  flavour: '<your_client_identifier>',
  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;
}
```

***

## Session & Document Utilities

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

### Account config (`getConfig`)

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()` returns what the server supports — models, languages, upload methods and audio formats.

***

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

***

## Authentication

The token you pass at init is used for every request. Two ways to keep it fresh:

**Automatic (recommended)** — register `onTokenRequired`; the SDK calls it on a 401 and retries the failed request:

```ts theme={null}
ekascribe.registerCallback('onTokenRequired', async () => await refreshToken());
```

**Manual** — `updateAuthTokens()` propagates a new token to all internal transports and the worker:

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

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

***

## Configuration

```ts theme={null}
interface EkaScribeConfig {
  access_token?: string;          // Bearer token for authentication
  env: 'PROD' | 'DEV';           // Environment
  flavour: string;                // Required — app variant identifier issued by Eka Care
  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
  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 Prebuilt Widget
}
```

## Recording Options

Passed to `startRecordingV2()`:

```ts theme={null}
interface StartRecordingV2Options {
  templates: string[];                   // Template IDs for extraction (required)
  model?: string;                        // 'pro' | 'lite'
  languageHint?: string[];               // Language codes for audio input
  transcriptLanguage?: string;           // Language code for transcript output
  uploadType?: string;                   // 'chunked' (default) | 'single'
  sessionMode?: string;                  // 'consultation' | 'dictation'
  patientDetails?: PatientDetails;       // Patient info
}
```

***

## API Reference

### Instance lifecycle

| Method                         | Returns         | Description                                                           |
| ------------------------------ | --------------- | --------------------------------------------------------------------- |
| `getEkaScribeInstance(config)` | `EkaScribe`     | Get (or create) the singleton instance for an `env` + `clientId`.     |
| `resetInstance()`              | `Promise<void>` | Full teardown — clears state, destroys the widget, removes callbacks. |

### Recording

| Method                                              | Returns                   | Description                                                                 |
| --------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------- |
| `startRecordingV2(options)`                         | `TStartRecordingResponse` | Create session + start mic + begin upload.                                  |
| `pauseRecording()`                                  | `void`                    | Pause VAD (mic stays open, no chunks created).                              |
| `resumeRecording()`                                 | `void`                    | Resume VAD processing.                                                      |
| `endRecording()`                                    | `TEndRecordingResponse`   | Stop mic, flush audio, wait for uploads, end session (triggers processing). |
| `retryUploadRecording()`                            | —                         | Retry chunks that failed to upload.                                         |
| `processPreRecordedAudio({ uploadUrl, audioFile })` | —                         | Upload a pre-recorded file instead of live recording.                       |

### Session

| Method                                             | Returns                               | Description                                              |
| -------------------------------------------------- | ------------------------------------- | -------------------------------------------------------- |
| `getSessionStatus(sessionId?, options?)`           | `SDKResult<GetSessionStatusResponse>` | Get status. Supports `poll` and `templateId` options.    |
| `cancelSession(sessionId?)`                        | —                                     | Cancel session (stops recorder, no server processing).   |
| `sessions.createSession(request)`                  | —                                     | Create a session without starting a recording.           |
| `sessions.endSession()`                            | —                                     | End a session explicitly (used with pre-recorded audio). |
| `sessions.getSessionHistory(request)`              | —                                     | Fetch previous sessions.                                 |
| `sessions.getSessionDetails(request)`              | —                                     | Session metadata, documents, presigned URLs.             |
| `sessions.patchSessionStatus(request, sessionId?)` | —                                     | Patch session (patient details, templates, etc.).        |
| `sessions.getConfig()`                             | `TGetConfigV2Response`                | Account config, including `my_templates`.                |
| `sessions.getDiscoveryDocument()`                  | —                                     | Server capabilities (models, languages, upload methods). |

### Documents

| Method                              | Returns | Description                                        |
| ----------------------------------- | ------- | -------------------------------------------------- |
| `documents.getDocument(documentId)` | —       | Fetch a document by ID with a fresh presigned URL. |

### Widget

| Method                    | Description                                                                           |
| ------------------------- | ------------------------------------------------------------------------------------- |
| `startForPatient(config)` | Show the widget and start a session for one patient. Requires `widget.enabled: true`. |

### Auth

| Method                               | Description                                                       |
| ------------------------------------ | ----------------------------------------------------------------- |
| `updateAuthTokens({ access_token })` | Update the Bearer token. Propagates to transports and the worker. |

***

## Callbacks

Register with `ekascribe.registerCallback(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`        | —                           | 401 received — **return** the new token string (10s timeout). |

### 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 — return the token, do not call resolve()
type TokenRequiredHandler = () => Promise<string>;
```

***

## Request / Response Types

### Recording responses

```ts theme={null}
interface TStartRecordingResponse {
  txn_id?: string;      // session id — present on success
  error_code?: string;  // see Error Codes below — present on failure
  message?: string;     // human-readable error message
}

interface TEndRecordingResponse {
  error_code?: string;  // e.g. 'audio_upload_failed'
  message?: string;
}
```

### Session

```ts theme={null}
interface CreateSessionResponse {
  session_id: string;
  status: SessionStatus;
  created_at: string;
  expires_at: string;
  upload_url: string;
  patient_details?: PatientDetails;
}

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 PatientDetails {
  oid?: string;
  name?: string;
  age?: string;
  gender?: string;
  mobile?: number;
}

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

***

## Error Handling

The SDK reports errors two ways, depending on the method.

**Recording methods** (`startRecordingV2`, `endRecording`) return an `error_code` field:

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

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

**Status methods** (`getSessionStatus`) return an `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 status = await ekascribe.getSessionStatus(sessionId);

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

console.log(status.data.transcript);
```

### Error Codes

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

***

## Deprecated Methods

These methods are from older SDK 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()` |

***

## Source and specification

* [EkaScribe TS SDK](https://github.com/eka-care/eka-js-sdk) — source for `@eka-care/ekascribe-ts-sdk`
