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

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

Step 4: Start Recording

Creates a session, starts the microphone, and begins chunked upload in one call.
Use clinical_notes_template for testing, or contact Eka Care to create a custom template for your use case.

Pause / Resume

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

Step 6: Poll for Results

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

Step 7: Clean Up

Flow Diagram


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.

Update a Session (Patch)

Update session properties after creation.

Two-Step Flow (Create Session + Record Separately)

Get Status for a Specific Template

Retry Failed Uploads

Update Auth Token


Configuration

Recording Options

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

Request / Response Types

Session

Recording

Error Handling

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

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

Serving the Worker

The worker file must be served as a static asset: Copy to your public directory:
Or use a CDN blob URL (avoids same-origin restrictions):
Or set a global override:
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:
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 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
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.
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():
Step 2 — Start recording with startRecordingV2(). Returns TStartRecordingResponse — check error_code, read the session id from 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):
Step 4 — End recording with endRecording(). On audio_upload_failed, retry with retryUploadRecording():
Step 5 — Poll for results with getSessionStatus() (same shape as the Alliance SDK — returns SDKResult):
Step 6 — Cancel instead of end (optional) — stops recording without triggering processing:
Step 7 — Clean upresetInstance() tears down the singleton (clears state, destroys the widget, removes callbacks). Call getEkaScribeInstance() again afterwards:

Full example

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:
Browser (script tag):
SharedWorker URL — pass sharedWorkerUrl in config. Resolve it with the wrapper’s helper:
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:
Step 2: Call startForPatient() per patient — the widget appears and the user drives it (pause, resume, stop). Results arrive via callbacks.
The widget handles startRecordingV2(), pauseRecording(), resumeRecording(), endRecording(), and getSessionStatus() internally.Widget state flow:
Wrapper-only helpers for fetching session history, details, and documents.getSessionHistory(request) — fetch previous sessions.
getSessionDetails(request) — detailed info including documents, context, and presigned URLs.
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).
patchSessionStatus(request, sessionId?) — update session properties.
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.
Response: TGetConfigV2Response
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()
Manually update the access token — propagates to all internal transports and the worker.
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.