@eka-care/medical-records-ts-sdk
v1.1.9
Published
TypeScript SDK for Eka Care Medical Records API
Readme
@eka-care/medical-records-ts-sdk
TypeScript SDK for the Eka Care Medical Records API. Offline-first, works in both browser and Electron renderer.
Table of Contents
- Installation
- Quick Start
- Configuration
- Authentication
- Records
- Cases
- Reactive UI Updates
- Logging
- Offline Support
- Electron Setup
- Error Handling
- Document Types
- Running the Demo
1. Installation
npm install @eka-care/medical-records-ts-sdk2. Quick Start
import { MedicalRecordsClient } from '@eka-care/medical-records-ts-sdk';
const sdk = new MedicalRecordsClient({
baseUrl: '',
defaultHeaders: {
'client-id': 'your-client-id',
'flavour': 'your-flavour',
'b-tid': 'your-business-id', // business tenant ID — sent on every API request
},
cache: { oid: 'logged-in-user-oid' },
onError: (err) => console.error(err),
});
sdk.setAuthToken('eyJhbGciOi...');
const docs = await sdk.listDocuments({ bid: 'your-business-id', patientId: 'oid' });3. Configuration
new MedicalRecordsClient(config: SDKConfig)| Field | Type | Required | Description |
|---|---|---|---|
| baseUrl | string | No | Override base URL. Pass '' for same-origin. Defaults to https://api.eka.care |
| environment | 'prod' \| 'dev' | No | prod = api.eka.care, dev = api.dev.eka.care. Ignored if baseUrl is set |
| defaultHeaders | Record<string, string> | Yes | Required Eka headers — see below |
| cache | CacheConfig | No | Enables IndexedDB offline cache |
| cache.oid | string | Yes (if cache) | Logged-in user's OID — default patient scope |
| transport | 'native' \| 'bridge' | No | native = browser fetch (default). bridge = Electron IPC |
| onError | (err: unknown) => void | No | Called on every SDK error |
| onUnauthorized | () => Promise<string \| undefined> | No | Called on 401 — return a fresh token to auto-retry |
| logLevel | 'debug' \| 'info' \| 'warn' \| 'error' \| 'none' | No | Minimum log level. Default: 'none' |
| onLog | (log: SDKLog) => void | No | Receives structured log entries at or above logLevel |
Required headers
defaultHeaders: {
'client-id': 'doc-web', // app identifier
'flavour': 'ekascribe-web', // app variant
'b-tid': 'BID', // business tenant ID — sent on every API request
}b-tid (business tenant ID) is required by the Eka API edge for routing and access control. It is merged into every HTTP request automatically.
Note: bid (business ID) is also passed per SDK call — it scopes IndexedDB queries client-side. Both carry the same value.
Full config example
const sdk = new MedicalRecordsClient({
baseUrl: 'https://api.eka.care',
defaultHeaders: {
'client-id': 'doc-web',
'flavour': 'ekascribe-web',
'b-tid': 'BID',
},
cache: { oid: 'USER_OID' },
transport: 'native',
logLevel: 'info',
onLog: ({ level, message, context }) => console.log(`[${level}] ${message}`, context),
onError: (err) => console.error('[SDK]', err),
onUnauthorized: async () => await refreshToken(),
});4. Authentication
sdk.setAuthToken('eyJhbGciOi...'); // set after login
sdk.setAuthToken(undefined); // clear (falls back to cookie auth)Sent as Authorization: Bearer <token> on every request.
On 401, onUnauthorized is called — return a fresh token to auto-retry once.
5. Records
Every records method requires bid and patientId:
const BID = 'your-business-id';
const OID = 'logged-in-user-oid';Linked profiles (primary + linked OIDs)
patientId is the primary OID — the profile that owns writes (add/edit/delete use it for the
X-Pt-Id header). To also surface records/cases from linked profiles (e.g. dependents), pass
linkedPatientIds. Read and list/group methods then return the union across
[patientId, ...linkedPatientIds], and the background sync fans out one fetch per OID (each with
its own X-Pt-Id). Omit linkedPatientIds for single-profile behavior — nothing changes.
const docs = await sdk.listDocuments({
bid: BID,
patientId: OID, // primary — owns writes
linkedPatientIds: ['dependent-oid-1', 'dependent-oid-2'], // also read these
});Supported on: listDocuments, listLocalDocuments, groupDocuments, getAllTags,
listCases, listLocalCases. Writes (addDocument, editDocument, deleteDocument,
createCase, …) always use the primary patientId.
List Documents
Delta sync — fetches only records newer than the local watermark.
// All documents
const docs = await sdk.listDocuments({ bid: BID, patientId: OID });
// With limit and sort
const docs = await sdk.listDocuments({
bid: BID, patientId: OID,
limit: 20, // default: no limit
sortBy: 'createdAt', // 'updatedAt' (default) | 'createdAt'
});
// Filter by record type
const labReports = await sdk.listDocuments({
bid: BID, patientId: OID,
filter: { recordType: 'lr' }, // lr | ps | dc | vc | in | iv | sc | op
});
// Filter by case
const caseRecords = await sdk.listDocuments({
bid: BID, patientId: OID,
filter: { caseId: 'case-uuid' },
});
// Stale-while-revalidate — show cached instantly, update when fresh arrives
const docs = await sdk.listDocuments({
bid: BID, patientId: OID,
onStale: (cached) => setDocs(cached),
});
setDocs(docs);
// Local DB only — no network call
const docs = await sdk.listLocalDocuments({ bid: BID, patientId: OID });ListDocumentsFilter fields:
| Field | Type | Description |
|---|---|---|
| recordType | DocumentType | Filter by document type ('lr', 'ps' etc.) — client-side |
| caseId | string | Filter to records belonging to a case — client-side |
| fileType | string | Filter by file type ('IMG', 'PDF', 'HTML') — client-side |
| u_at__gt | number | Only records updated after this epoch — sent to server |
Group Documents
Groups records from local DB — no network call.
// Group by document type
const groups = await sdk.groupDocuments({
bid: BID, patientId: OID,
groupBy: 'recordType',
});
// → [{ key: 'lr', label: 'Lab Report', records: [...] }, ...]
// Group by month (default)
const groups = await sdk.groupDocuments({
bid: BID, patientId: OID,
groupBy: 'date',
dateFormat: 'month', // 'day' | 'month' (default) | 'year'
dateField: 'documentDate', // 'documentDate' (default) | 'createdAt'
});
// → [{ key: '2025-05', label: 'May 2025', records: [...] }, ...]GroupDocumentsOptions:
| Field | Type | Default | Description |
|---|---|---|---|
| groupBy | 'recordType' \| 'date' | required | How to group |
| dateFormat | 'day' \| 'month' \| 'year' | 'month' | Granularity when groupBy: 'date' |
| dateField | 'documentDate' \| 'createdAt' | 'documentDate' | Which date to group by |
| sortBy | 'updatedAt' \| 'createdAt' | 'updatedAt' | Sort within each group |
| filter | ListDocumentsFilter | — | Same filters as listDocuments |
Download a Document
Cache-first — returns blobs from IndexedDB if previously fetched, otherwise downloads from S3 and caches.
const files = await sdk.downloadFile({ bid: BID, patientId: OID, documentId: 'abc-uuid' });
for (const file of files) {
console.log(file.blob); // Blob — PDF / image / HTML
console.log(file.fileType); // 'IMG' | 'PDF' | 'HTML'
console.log(file.filename); // e.g. 'abc12345_0.pdf'
console.log(file.fromCache); // true = served from IndexedDB, zero network
}
// Trigger browser download
const a = document.createElement('a');
a.href = URL.createObjectURL(files[0].blob);
a.download = files[0].filename;
a.click();Get Smart Report
Cache-first — returns the AI-extracted smart report from IndexedDB if available, otherwise fetches from API.
const report = await sdk.getSmartReport({
bid: BID, patientId: OID,
documentId: 'abc-uuid',
});
if (report) {
console.log(report.verified); // AI-verified fields
console.log(report.unverified); // AI-unverified fields
}
// null = document has no smart reportUpload a Document
const file = input.files[0];
await sdk.addRecord({
bid: BID, patientId: OID,
batchRequests: [{
dt: 'lr',
dd_e: Math.floor(Date.now() / 1000),
cases: ['case-uuid'], // optional — assign to case
files: [{ contentType: 'application/pdf', file_size: file.size }],
}],
files: [[file]],
filenames: [[file.name]],
});Offline: record + blob saved to IndexedDB as upload_failure. Auto-retried on next sdk.sync().
File constraints:
| | Limit |
|---|---|
| Max batch items | 5 |
| Max files per batch | 10 |
| Image max | 10 MB |
| PDF max | 25 MB |
| Supported types | image/jpeg image/png application/pdf |
Describe a Document
Returns file URLs, smart report, tags.
const detail = await sdk.describeDocument({
bid: BID, patientId: OID,
documentId: 'abc-uuid',
});
// detail.files[0].asset_url — signed S3 URL
// detail.smart_report — AI-extracted fields
// detail.tags — user tagsEdit a Document
Local-first — patches DB immediately, syncs to server in background.
await sdk.editRecord({
bid: BID, patientId: OID,
documentId: 'abc-uuid',
data: {
dt: 'ps', // new document type
dd_e: 1748000000, // new document date (epoch)
cases: ['case-id-1'], // link to cases (replaces existing)
tg: ['blood', 'routine'], // tags (replaces existing)
},
});Delete a Document
Soft-deletes locally (hidden immediately), hard-deletes after server confirms. Also removes the document from any linked cases in local DB.
await sdk.deleteRecord({ bid: BID, patientId: OID, documentId: 'abc-uuid' });6. Cases
Cases act as folders for records. A record can belong to multiple cases.
List Cases
const cases = await sdk.listCases({ bid: BID, patientId: OID });
// Local DB only:
const cases = await sdk.listLocalCases({ bid: BID, patientId: OID });
// Records inside a case:
const docs = await sdk.listLocalDocuments({
bid: BID, patientId: OID,
filter: { caseId: 'case-uuid' },
});Create a Case
await sdk.createCase({
bid: BID, patientId: OID,
data: {
id: crypto.randomUUID(),
display_name: 'Follow-up visit',
type: 'OP', // EM | IP | OP | HH
occurred_at: Math.floor(Date.now() / 1000), // required
},
});Update a Case
await sdk.updateCase({
bid: BID, patientId: OID,
caseId: 'case-uuid',
data: { display_name: 'New name', type: 'IP', occurred_at: 1748000000 },
});Delete a Case
Deletes from server and removes the case from all linked records' caseIDs in local DB.
await sdk.deleteCase({ bid: BID, patientId: OID, caseId: 'case-uuid' });7. Reactive UI Updates
Subscribe to DB change events — the UI auto-updates on any write (upload, edit, delete, sync).
// Subscribe once (e.g. in useEffect)
const unsub = sdk.subscribe('records:changed', () => {
sdk.listLocalDocuments({ bid, patientId }).then(setRecords);
});
const unsubCases = sdk.subscribe('cases:changed', () => {
sdk.listLocalCases({ bid, patientId }).then(setCases);
});
// Cleanup on unmount
return () => { unsub(); unsubCases(); };Events:
records:changed— fired after any record write (upload, edit, delete, sync)cases:changed— fired after any case write
Pending sync count:
const { records, cases } = await sdk.getPendingCounts();
// records — count of unsynced records (uploading / edited / archived)
// cases — count of unsynced cases (not yet created or edited on server)8. Logging
All internal SDK logs are prefixed with EkaMR: and routed through your onLog callback.
import type { SDKLog } from '@eka-care/medical-records-ts-sdk';
const sdk = new MedicalRecordsClient({
logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'none' (default)
onLog: (log: SDKLog) => {
// Forward to your logging system
console.log(`[${log.level}] ${log.message}`, log.context);
// or: Sentry.addBreadcrumb({ message: log.message, data: log.context })
// or: datadogLogs.logger.info(log.message, log.context)
},
});Log levels:
| Level | What is logged |
|---|---|
| debug | Watermark values, cache hit/miss, per-record sync results |
| info | Sync started/completed, N records synced, upload complete |
| warn | Network failed → serving stale cache, server sync failed → will retry |
| error | S3 upload failed with document IDs |
| none | All logging disabled (default) |
SDKLog shape:
interface SDKLog {
level: 'debug' | 'info' | 'warn' | 'error';
message: string; // e.g. "EkaMR: sync: started"
context?: Record<string, unknown>; // structured data (count, documentId, etc.)
timestamp: number; // Unix milliseconds
}9. Offline Support
The SDK stores everything in IndexedDB and syncs in the background.
What happens offline
| Action | Offline behavior |
|---|---|
| listDocuments | Returns cached records immediately |
| listCases | Returns cached cases immediately |
| addRecord | Saves record + blob as upload_failure — retried on reconnect |
| editRecord | Patches DB (isEdited: true) — synced on reconnect |
| deleteRecord | Soft-deletes (isArchived: true) — hard-deleted on reconnect |
| createCase | Saves locally (isRemoteCreated: false) — created on reconnect |
Sync order
Sync always runs: cases → uploads → edits + deletes (parallel).
// Auto-runs on every SDK init (page load)
// Manual trigger:
window.addEventListener('online', () => sdk.sync());
await sdk.sync();Clear cache on logout
import { clearDb } from '@eka-care/medical-records-ts-sdk';
await clearDb();10. Electron Setup
IndexedDB works natively in the Electron renderer process. Only the transport differs:
// Electron
new MedicalRecordsClient({ transport: 'bridge', cache: { oid }, defaultHeaders: {...} })
// Browser
new MedicalRecordsClient({ transport: 'native', cache: { oid }, defaultHeaders: {...} })bridge routes Eka API calls through window.networkApi (IPC → main → net.fetch).
S3 uploads always use fetch() directly — the IPC bridge cannot carry binary data.
11. Error Handling
import { EkaCareApiError, UploadFailedError } from '@eka-care/medical-records-ts-sdk';
try {
await sdk.addRecord({ ... });
} catch (err) {
if (err instanceof UploadFailedError) {
// Auth succeeded but S3 upload failed — record saved as upload_failure
// sdk.sync() retries automatically
console.error('Failed IDs:', err.failures.map(f => f.documentId));
} else if (err instanceof EkaCareApiError) {
console.error(err.message, err.statusCode, err.responseBody);
}
}editRecord, deleteRecord, createCase, updateCase, deleteCase never throw on network failure — local state is always updated and server sync retries automatically.
12. Document Types
| Code | Label |
|---|---|
| ps | Prescription |
| lr | Lab Report |
| dc | Discharge Summary |
| vc | Vaccine Certificate |
| in | Insurance |
| iv | Invoice |
| sc | Scan |
| op | Other |
import { DOCUMENT_TYPE_LABELS, DOCUMENT_TYPE_OPTIONS } from '@eka-care/medical-records-ts-sdk';
DOCUMENT_TYPE_OPTIONS // [{ value: 'lr', label: 'Lab Report' }, ...] — for dropdowns
DOCUMENT_TYPE_LABELS // { lr: 'Lab Report', ps: 'Prescription', ... } — for display13. Running the Demo
packages/ui is a React playground — not published.
# Install
npm install && cd packages/ui && npm install
# Credentials — edit packages/ui/src/example/sdk/client.ts
export const OID = 'your-oid';
export const BUSINESS_ID = 'your-business-id';
# Start
cd packages/ui && npm run dev
# Open https://test.eka.care:5173Building
npm run build # TypeScript → dist/Publishing
npm version patch
npm publish --access public