Skip to main content
Capture and process audio in the browser and generate structured medical documentation through Eka Care’s voice transcription service.
Two packages, one protocol. There are two ways to integrate in TypeScript:
  • med-scribe-alliance-ts-sdk — the open-source MedScribe Alliance 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) 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.

Prerequisites

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

Installation

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 →

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:
EnvironmentbaseUrl
Productionhttps://api.eka.care/voice/v1
Developmenthttps://api.dev.eka.care/voice/v1
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
});
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 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.

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.).
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.
// 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.
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;
Use clinical_notes_template for testing, or contact Eka Care to create a custom template for your use case.

Pause / Resume

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

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

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

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

Retry Failed Uploads

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

Update Auth Token

client.setAccessToken('new-bearer-token');

Configuration

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

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

MethodReturnsDescription
init()SDKResult<void>Fetch discovery document. Called automatically by startRecording.
reset()Promise<void>Stop recording, clear all state and caches.

Recording

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

Session

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

MethodReturnsDescription
getDiscoveryDocument()DiscoveryDocument | nullRaw discovery document.
getDiscoveryConfig()SDKResult<ResolvedConfig>Resolved config from discovery.
refreshDiscovery()SDKResult<ResolvedConfig>Force-refresh discovery.

Auth

MethodDescription
setAccessToken(token)Update Bearer token. Propagates to transport, recorder, and worker.

Callbacks

Register with client.registerCallback(name, handler), remove with client.removeCallback(name, handler).
CallbackPayloadDescription
onRecordingStateChangeRecordingStateChangeEventRecording started, paused, resumed, or ended.
onAudioEventAudioEventSpeech detection, silence warnings, chunk ready.
onUploadEventUploadEventUpload progress and failures.
onSessionEventSessionEventSession created, ended, status updates.
onErrorErrorEventVAD, worker, transport, or validation errors.
onTokenRequiredTokenRequiredEvent401 received — call event.resolve(newToken) to retry.

Payload Shapes

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

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

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:
type SDKResult<T> =
  | { success: true; data: T }
  | { success: false; error: ScribeError };
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

ErrorHTTPDescription
ScribeErrorBase error class
ValidationError400Invalid request or config
AuthenticationError401Auth failed (after token refresh attempt)
ForbiddenError403Access denied
SessionNotFoundError404Session doesn’t exist
SessionExpiredError410Session expired
RateLimitError429Rate limit exceeded
DiscoveryErrorDiscovery fetch/parse failed
TransportErrorNetwork / IPC failure
WorkerErrorSharedWorker failure
UploadErrorAudio 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

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:
cp node_modules/med-scribe-alliance-ts-sdk/dist/worker.bundle.js public/
Or use a CDN blob URL (avoids same-origin restrictions):
import { createWorkerBlobUrl } from 'med-scribe-alliance-ts-sdk';

const workerUrl = await createWorkerBlobUrl();
const client = new ScribeClient({
  baseUrl: '...',
  workerScriptUrl: workerUrl,
});
Or set a global override:
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:
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)

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 above is the recommended path.
@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.
npm install @eka-care/ekascribe-ts-sdk
# or
yarn add @eka-care/ekascribe-ts-sdk
npm package →

Alliance → wrapper method map

OperationAlliance (ScribeClient)Eka wrapper
Create clientnew ScribeClient(config)getEkaScribeInstance(config) (singleton)
Start recordingstartRecording()SDKResultstartRecordingV2()TStartRecordingResponse (txn_id, error_code)
Token refreshonTokenRequiredevent.resolve(token)onTokenRequiredreturn token
Retry uploadsretryFailedUploads()retryUploadRecording()
Teardownreset()resetInstance()
Get outputgetSessionStatus(id, { poll })getSessionStatus(id, { poll }) (same)
The wrapper uses a singleton patterngetEkaScribeInstance() 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:
EnvironmentbaseUrl
Productionhttps://api.eka.care/voice/v1
Developmenthttps://api.dev.eka.care/voice/v1
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);
Production APIs require a secure (HTTPS) origin — they will not work from http://localhost. Recommended: use 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.
  • 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.
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.
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():
// 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:
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!;
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.
Step 3 — Pause / Resume (optional, during recording):
ekascribe.pauseRecording();
ekascribe.resumeRecording();
Step 4 — End recording with endRecording(). On audio_upload_failed, retry with retryUploadRecording():
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):
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:
await ekascribe.cancelSession();            // current session
await ekascribe.cancelSession('session-id'); // or by id
Step 7 — Clean upresetInstance() tears down the singleton (clears state, destroys the widget, removes callbacks). Call getEkaScribeInstance() again afterwards:
await ekascribe.resetInstance();

Full example

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();
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:
'use client';

import { getEkaScribeInstance } from '@eka-care/ekascribe-ts-sdk';
Browser (script tag):
<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:
import { createWorkerBlobUrl } from '@eka-care/ekascribe-ts-sdk';

const workerUrl = await createWorkerBlobUrl();
const ekascribe = getEkaScribeInstance({ /* ... */ sharedWorkerUrl: workerUrl });
// Remember to URL.revokeObjectURL(workerUrl) when done.
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:
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.
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
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;
}
Wrapper-only helpers for fetching session history, details, and documents.getSessionHistory(request) — fetch previous sessions.
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.
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).
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.
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.
const config = await ekascribe.sessions.getConfig();
config.data?.my_templates.forEach((t) => console.log(t.id, t.name));
Response: TGetConfigV2Response
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;
};
Discoveryekascribe.sessions.getDiscoveryDocument() mirrors the Alliance discovery method.
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()
const result = await ekascribe.processPreRecordedAudio({
  uploadUrl: session.upload_url,       // from createSession response
  audioFile: audioBlob,                // File or Blob
});
Manually update the access token — propagates to all internal transports and the worker.
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).
The wrapper’s TStartRecordingResponse / TEndRecordingResponse carry an error_code field (unlike the Alliance SDKResult error classes).
Error CodeDescription
microphoneMicrophone access error (permission denied or unavailable)
txn_init_failedFailed to initialize session
txn_limit_exceededMaximum concurrent sessions exceeded
internal_server_errorUnexpected server-side error
end_recording_failedFailed to end recording
audio_upload_failedAudio file upload to server failed
txn_commit_failedCommit call failed
txn_status_mismatchInvalid operation for current session state
network_errorNetwork connectivity issue
unknown_errorUnclassified error
unauthorizedAuthentication failed (invalid or expired token)
forbiddenInsufficient permissions
These methods are from older wrapper versions. They still work but are not recommended for new integrations.
Deprecated MethodUse Instead
initTransaction() + startRecording()startRecordingV2()
getTemplateOutput()getSessionStatus() with polling
getOutputTranscription()getSessionStatus() with polling
commitTransactionCall()Handled automatically by endRecording()
stopTransactionCall()Handled automatically by endRecording()

Refer to the MedScribe Alliance Protocol, the MedScribe Alliance TS SDK, and the EkaScribe TS SDK repositories for implementation details.