# KodeMed Integration Guide > For Software Developers & System Integrators ## Table of Contents 1. [Overview](#1-overview) 2. [Integration Options](#2-integration-options) 3. [Option A - COM DLL Integration](#3-option-a---com-dll-integration) 4. [Option B - REST API Integration](#4-option-b---rest-api-integration) 5. [Option C - WebSocket Integration](#5-option-c---websocket-integration) 6. [Authentication - OAuth2 and OIDC](#6-authentication---oauth2-and-oidc) 7. [Data Formats](#7-data-formats) 8. [Configuration Reference](#8-configuration-reference) 9. [CodingClient Setup](#9-codingclient-setup) 10. [Post-Coding Webhook](#10-post-coding-webhook) 11. [API Reference](#11-api-reference) 12. [Troubleshooting](#12-troubleshooting) 13. [Cloud & Orchestration](#13-cloud--orchestration) --- ## 1. Overview KodeMed is a clinical coding system for Swiss hospitals that provides interactive coding of ICD-10, CHOP, and DRG classifications in the SpiGes format. It can be integrated into Hospital Information Systems (HIS) via three methods: | Method | Use Case | Language | Auth | |--------|----------|----------|------| | **COM DLL** | Direct HIS integration (.NET, VB, C#, Delphi) | Any COM-compatible | OAuth2/OIDC | | **REST API** | Any language/platform via HTTP | Any | OAuth2 Bearer | | **WebSocket** | Real-time bidirectional via CodingClient | Any | OAuth2 Bearer | ### Architecture All three methods ultimately open the **embedded WebView2 browser** in the DLL for the interactive coding session. The coding UI is never opened in an external browser. ``` Hospital Information System (HIS) │ ├── Option A: COM DLL (direct) ──> KodeMed.dll ──> Embedded WebView2 Coding UI │ ├── Option B: REST API ── POST ──> KodeMed Server ─── WS push ──> CodingClient (tray app) │ │ │ │ │ KodeMed.dll + Embedded WebView2 │ │ └── Option C: WebSocket ── WS ───> KodeMed Server ─── WS push ──> CodingClient (tray app) │ │ │ KodeMed.dll + Embedded WebView2 │ KodeMed DataServer (ICD-10, CHOP catalogs) KodeMed GrouperServer :8082 (DRG grouping: SwissDRG, TARPSY, ST Reha) ``` > **Key:** In Options B and C, the 3rd-party app sends data to the KodeMed Server **with a > `targetUserId` parameter** identifying which coder should receive the session. The server > looks up the coder's WebSocket connection and pushes a `CODING_SESSION_LAUNCH` message > to their **CodingClient** tray app. The CodingClient calls `Coding.DoCodingWithFormat()` > which opens the same embedded WebView2 browser as Option A. Results flow back through > the server to the 3rd-party app. > > **Prerequisite:** The target coder must be **signed in and connected** via CodingClient. > Use `GET /api/v1/coding/clients` to discover available coders before sending sessions. --- ## 2. Integration Options ### Decision Matrix | Criteria | COM DLL | REST API | WebSocket | |----------|---------|----------|-----------| | **Complexity** | Low | Medium | Medium | | **Real-time** | Yes (embedded UI) | No (poll or webhook) | Yes (events pushed via WS) | | **Platform** | Windows only | Any | Any | | **Requires CodingClient** | No (DLL runs in-process) | Yes (coder must be signed in) | Yes (coder must be signed in) | | **Offline** | Partial (validation) | No | No | | **User interaction** | Embedded WebView2 browser | Embedded WebView2 (via CodingClient) | Embedded WebView2 (via CodingClient) | | **Best for** | .NET/VB HIS apps on same machine | Web apps, cross-platform HIS | 3rd party apps needing real-time events | --- ## 3. Option A - COM DLL Integration ### Prerequisites - **Windows only** (10/11, Server 2016+). Linux/macOS not supported - .NET 9.0 Runtime (Windows) - Microsoft Edge WebView2 Runtime (auto-installed with Windows 11, or download from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)) - `KodeMed.comhost.dll` registered via COM (HKCU, no admin required) — installed automatically by the KodeMed installer - `KodeMed.dll` — main managed assembly (must be in the same directory as comhost.dll) ### Quick Start (C#) ```csharp using KodeMed; using KodeMed.Models; // 1. Create instance (one per application lifetime, reusable for multiple sessions) var coding = new Coding(); // 2. Configure - XML or JSON, auto-detected. Call once at startup. string config = @"{ ""serverUrl"": ""https://kodemed.example.com"", ""codingUIUrl"": ""https://kodemed.example.com"", ""oauth2Url"": ""https://sso.example.com/auth"", ""oauth2Realm"": ""kodemed"", ""oauth2ClientId"": ""kodemed-dll"" }"; coding.SendConfig(config); // returns false if config is invalid → check coding.LastError // 3. Process case data // DoCodingWithFormat() does ALL of this automatically: // - Authenticates via OAuth2 (opens login popup if first time) // - Creates a session on the KodeMed server // - Opens the embedded WebView2 browser with the coding UI // - Waits for the user to code and close the browser // - Returns true if the session completed without errors string spigesXml = File.ReadAllText("case_data.xml"); bool success = coding.DoCodingWithFormat(spigesXml, FormatType.SpiGes); // 4. Check what the user did CodingAction action = coding.LastCodingAction; if (action == CodingAction.Applied) { // User applied changes → save the modified SpiGes XML back to your HIS string modifiedXml = coding.GetResultData(); SaveToHIS(modifiedXml); } else if (action == CodingAction.Discarded) { // User explicitly discarded → use original data (no changes) } else if (action == CodingAction.Cancelled) { // User closed the browser window without choosing → use original data } else if (action == CodingAction.Timeout) { // Session timed out → use original data } else if (action == CodingAction.Error) { Console.WriteLine($"Error: {coding.LastError}"); } // Optional: Get full statistics (DRG, cost weight, PCCL, etc.) string stats = coding.GetResults(); // XML or JSON based on outputFormat config // 5. Clean up when your application exits coding.DisposeClient(); ``` ### Quick Start (VB.NET) ```vb ' COM - works from VB.NET, VBA, Delphi, etc. Dim coding As Object = CreateObject("KodeMed.Coding") ' Configure (XML or JSON, auto-detected) coding.SendConfig(configXml) ' Option 1: Headless (no browser) Dim headless As Boolean = coding.DoCoding(spigesData, 0) ' 0 = SpiGes ' Option 2: Interactive with browser (OAuth2 + WebView2) Dim interactive As Boolean = coding.DoCodingWithFormat(spigesData, 0) ' 0 = SpiGes ' Get results (XML or JSON based on outputFormat config) If interactive Then Dim stats As String = coding.GetResults() End If ' Clean up coding.DisposeClient() ``` ### Lifecycle ``` SendConfig(xml/json) ← Load server URLs, OAuth2, settings │ ▼ DoCodingWithFormat(data, fmt) ← Validates data, authenticates via OAuth2, │ creates server session, opens WebView2 browser │ ⚠️ BLOCKING: holds the calling thread until session ends │ ├── User applies changes ──> CodingAction.Applied + ModifiedData ├── User discards ──────────> CodingAction.Discarded ├── User closes window ─────> CodingAction.Cancelled └── Timeout / Error ────────> CodingAction.Timeout / Error │ ▼ GetResults() ← XML/JSON with statistics, DRG results, modified data ``` ### ⚠️ Blocking Behavior **`DoCodingWithFormat()` is a blocking call.** It does not return until the user completes the coding session (Apply, Discard, Cancel) or the session times out. This means: - The calling thread is **blocked** for the entire duration of the interactive coding session - A typical coding session lasts **2–15 minutes** depending on case complexity - If called from a HIS main thread, the HIS UI will **freeze** until the session ends **Recommendation for COM DLL (Option A):** - Call `DoCodingWithFormat()` on a **background thread** (e.g., `Task.Run()` in C#, `BackgroundWorker` in VB.NET) - Use the `CodingSessionCompleted` event to receive results asynchronously - Or call from a separate process/service **Why CodingClient is preferred (Options B & C):** For REST API and WebSocket integration, the **CodingClient tray app** architecture avoids the blocking problem entirely: ``` Option A (COM DLL — blocking): HIS thread ── DoCodingWithFormat() ──── BLOCKED 2-15 min ──── returns result (HIS freezes) Options B & C (REST/WebSocket — non-blocking): HIS thread ── POST /coding/session ── returns immediately (HTTP 201) │ ▼ (server pushes via WebSocket) CodingClient ── DoCodingWithFormat() ── BLOCKED (but on coder's machine, not HIS) │ ▼ (results flow back via server) HIS thread ── GET /session/{id} ── gets completed results (or webhook notification) ``` | Aspect | COM DLL (Option A) | REST/WS via CodingClient (B & C) | |--------|--------------------|----------------------------------| | **HIS thread** | Blocked during coding | Free immediately after POST | | **Where blocking occurs** | In HIS process | In CodingClient (separate process) | | **HIS availability** | Frozen during session | Fully responsive | | **Result delivery** | Synchronous return | Poll, webhook, or WS event | | **Best for** | Simple desktop HIS | Web-based HIS, multi-user environments | > **Summary:** If the HIS cannot tolerate a blocked thread for several minutes, use > **Option B (REST)** or **Option C (WebSocket)** with CodingClient. The blocking call > runs on the coder's workstation instead of the HIS server/process. ### Events ```csharp // Status changes: NotInitialized → Configured → DataLoaded → Processing → Completed coding.StatusChanged += (ProcessingStatus status) => { Console.WriteLine($"Status: {status}"); }; // Errors coding.Error += (string errorMessage) => { Console.WriteLine($"Error: {errorMessage}"); }; // Progress (0-100%) coding.Progress += (int percentage, string message) => { progressBar.Value = percentage; }; // Authentication state changes (sign in, sign out, token refresh) coding.AuthenticationStateChanged += (bool isAuthenticated, string username, string message) => { if (isAuthenticated) lblUser.Text = $"Signed in as {username}"; }; // WebSocket connection state changes coding.ConnectionStateChanged += (ConnectionState state, string message) => { lblConnection.Text = $"Server: {state}"; // Connected, Reconnecting, Disconnected... }; // Coding session completed (fired when user closes the WebView2 browser) coding.CodingSessionCompleted += (CodingAction action, bool discarded, string resultData) => { if (action == CodingAction.Applied) { // resultData contains the modified SpiGes XML SaveToHIS(resultData); } // For Discarded, Cancelled, Timeout, Error → use your original data }; ``` ### Headless Mode (No UI) `DoCoding()` validates and transforms the data server-side **without opening the WebView2 browser**. Use this when the HIS only needs validation, format transformation, or grouping results without human interaction. ```csharp // Headless: validates data, sends to server for grouping, returns results (no browser) bool result = coding.DoCoding(spigesXml, FormatType.SpiGes); string stats = coding.GetResults(); // Contains DRG grouping, validation errors, etc. ``` > **Note:** Headless mode is only available via COM DLL (Option A). Options B and C always > route to a CodingClient which opens the interactive WebView2 UI. --- ## 4. Option B - REST API Integration ### Swagger / OpenAPI The KodeMed Server exposes an interactive API explorer via **Swagger UI**: ``` https:///swagger ``` Use Swagger to: - Browse all available REST endpoints with their request/response schemas - Try out API calls directly from the browser (click "Try it out") - Generate client code in any language from the OpenAPI spec - Download the OpenAPI JSON spec at `/swagger/v1/swagger.json` > **Tip:** When testing authenticated endpoints in Swagger, click the "Authorize" button > and enter your Bearer token: `Bearer `. You can get a token from > your OIDC provider using the standard token endpoint (see [Authentication](#6-authentication---oauth2-and-oidc)). ### How Routing Works In Options B and C, the 3rd-party app **does not** talk to KodeMed.dll directly. Instead: 1. **Coders sign in** via CodingClient → CodingClient connects WebSocket to the server 2. **Server registers** each connected CodingClient by the user's OAuth2 identity (from JWT: `email`, `sub`, `preferred_username`) 3. **3rd-party app queries** which coders are online: `GET /api/v1/coding/clients` 4. **3rd-party app sends** case data to the server with `targetUserId` specifying the coder 5. **Server routes** the session to the coder's CodingClient via WebSocket 6. **CodingClient opens** embedded WebView2 → coder works → results flow back through the server ``` Server 3rd-party App ┌────────────────────┐ CodingClient ┌──────────────┐ │ │ ┌──────────────────┐ │ │─ GET ───>│ /coding/clients │ │ maria@hospital.ch│ │ │<─ list ──│ → maria, pedro │ │ (WebSocket conn) │ │ │ │ │ │ │ │ │─ POST ──>│ /coding/session │ │ │ │ │ target: │ → lookup maria │─ WS ─>│ CODING_SESSION │ │ │ maria │ → route to her │ │ _LAUNCH │ │ │ │ │ │ │ │ │ │<─ sessId─│ │ │ v │ │ │ │ │ │ DoCodingWithFmt()│ │ │ │ │ │ → WebView2 UI │ │ │ │ │ │ │ │ │ │─ GET ───>│ /session/{id} │<─ WS ─│ CODING_APPLIED │ │ │<─ result─│ → completed data │ │ │ └──────────────┘ └────────────────────┘ └──────────────────┘ ``` > **Key:** The `targetUserId` must be the user ID (JWT `sub` claim) of a coder who is > **currently signed in and connected** via CodingClient. The 3rd-party app discovers > available coders via `GET /api/v1/coding/clients` before sending a session. > If the target coder is offline, the session is still created but `targetUserNotified` > will be `false` in the response. ### Step 1: Discover Connected Coders ```http GET /api/v1/coding/clients Authorization: Bearer ``` **Response:** ```json { "clients": [ { "userId": "a1b2c3d4-uuid-maria", "instanceId": "dll-abc-123", "connectedSince": "2026-02-12T08:00:00Z", "status": "idle" }, { "userId": "e5f6g7h8-uuid-pedro", "instanceId": "dll-def-456", "connectedSince": "2026-02-12T09:30:00Z", "status": "coding" } ], "count": 2 } ``` | Field | Description | |-------|-------------| | `userId` | The coder's OAuth2 user ID (JWT `sub` claim). Use this as `targetUserId` when creating sessions | | `instanceId` | The CodingClient WebSocket instance ID | | `connectedSince` | When the CodingClient connected | | `status` | `idle` = available, `coding` = session in progress | ### Step 2: Create a Coding Session ```http POST /api/v1/coding/session Authorization: Bearer Content-Type: application/json { "format": "spiges", "data": "...", "targetUserId": "maria-uuid-from-oidc-provider" } ``` | Field | Required | Description | |-------|----------|-------------| | `format` | Yes | `"spiges"`, `"bfs"`, or `"custom"` | | `data` | Yes | The case data (SpiGes XML, BFS, etc.) | | `targetUserId` | Yes* | User ID (JWT `sub` claim) of the coder. When set, session is created for this user and `CODING_SESSION_LAUNCH` is sent to their CodingClient. *Optional if you want to create a session for yourself. | | `dataHash` | No | SHA256 hash of the data for duplicate detection | | `source` | No | `"DLL"`, `"BROWSER"`, or `"API"`. Auto-set to `"API"` when `targetUserId` is used | | `forceClose` | No | If `true`, force-close any existing DLL session for the target user | > **No `enterpriseId` needed:** The enterprise ID is already inside the SpiGes data > (``). The server extracts it from the data automatically. **Success Response (201 Created):** ```json { "sessionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "redirectUrl": "https://kodemed.example.com/spiges/session/f47ac10b-.../cases", "isExisting": false, "status": "PENDING", "dataHash": "a1b2c3d4...", "source": "API", "hasConflict": false, "targetUserNotified": true } ``` | Response Field | Description | |---------------|-------------| | `sessionId` | UUID of the created session | | `redirectUrl` | URL for the coding UI (used internally by CodingClient) | | `isExisting` | `true` if a session with the same data hash was reused | | `status` | Session status: `PENDING`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED`, `EXPIRED` | | `source` | `"API"` when `targetUserId` was used, `"DLL"` or `"BROWSER"` otherwise | | `targetUserNotified` | `true` if `CODING_SESSION_LAUNCH` was delivered to the coder's CodingClient, `false` if coder is offline, `null` if `targetUserId` was not used | **Error: DLL session conflict (409 Conflict):** ```json { "hasConflict": true, "conflictSessionId": "existing-session-uuid", "conflictInstanceId": "dll-instance-id", "conflictCreatedAt": "2026-02-12T08:00:00Z", "conflictExpiresInMinutes": 45, "conflictMessage": "Ya tiene una sesión activa abierta..." } ``` > **Note:** Use `forceClose: true` to automatically cancel the conflicting session. ### Step 3: Check Session Status / Get Results ```http GET /api/v1/coding/session/{sessionId} Authorization: Bearer ``` **Response (metadata):** ```json { "sessionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "format": "spiges", "status": "COMPLETED", "createdAt": "2026-02-12T10:00:00Z", "hasData": true } ``` **Get result after completion:** ```http GET /api/v1/coding/session/{sessionId}/result ``` ```json { "sessionId": "f47ac10b-...", "action": "APPLIED", "hasData": true } ``` **Get the modified data:** ```http GET /api/v1/coding/session/{sessionId}/result/data ``` Returns raw XML/JSON with `Content-Type: application/xml` or `application/json`. | Status | Description | |--------|-------------| | `PENDING` | Session created, waiting for coder to start | | `IN_PROGRESS` | Coder has the WebView2 UI open | | `COMPLETED` | Session finished (check `action` field: `APPLIED`, `DISCARDED`, `CANCELLED`, `TIMEOUT`) | | `EXPIRED` | Session timed out without being opened | | `CANCELLED` | Session was cancelled | > **Polling vs Webhook:** 3rd-party apps can poll `GET /session/{id}` to check status, or configure a [post-coding webhook](#10-post-coding-webhook) for automatic result delivery. --- ## 5. Option C - WebSocket Integration ### When to use WebSocket instead of REST API Both REST (Option B) and WebSocket (Option C) use the **same routing mechanism**: the 3rd-party app sends case data with a `targetUser`, and the server routes it to the coder's CodingClient. The difference is **how your app receives results**: | | REST API (Option B) | WebSocket (Option C) | |---|---|---| | **Send data** | `POST /api/v1/coding/session` | `CODING_SESSION_LAUNCH` message | | **Get results** | Poll `GET /session/{id}` or use webhook | Real-time event pushed to your connection | | **Connection** | Stateless HTTP | Persistent bidirectional connection | | **Best for** | Simple integrations, batch processing | Apps that need immediate notifications | ### Concepts **Two separate WebSocket endpoints exist on the server:** | Endpoint | Who connects | Purpose | |----------|-------------|---------| | `/ws/dll` | CodingClient tray app (coders) | Receives `CODING_SESSION_LAUNCH`, sends back results | | `/ws/app` | Your 3rd-party application (planned) | Sends sessions, receives real-time events | > **Note:** The `/ws/app` endpoint for 3rd-party WebSocket connections is planned for a > future release. Currently, 3rd-party apps should use the **REST API (Option B)** which > routes sessions to coders via the server. The REST API provides the same routing and > notification functionality via `targetUserId`. **`instanceId`** is a unique identifier that **your app generates** to identify its WebSocket connection. It can be: - A UUID: `"550e8400-e29b-41d4-a716-446655440000"` (auto-generated) - A meaningful name: `"his-radiology-001"`, `"scheduling-app"`, `"lab-system-west"` The server uses `instanceId` to track your connection and route events back to you. If your app reconnects, use the same `instanceId` to resume receiving events for sessions you already created. ### Complete Flow (step by step) ``` Your 3rd-party App KodeMed Server CodingClient (maria) ────────────────── ────────────── ──────────────────── Maria signs in via OAuth2 CodingClient connects to /ws/dll Server registers: with Bearer token in header "maria@hospital.ch" → online ◄──── WS connected ──── Step 1: Connect ────────────────── ws = new WebSocket( Server registers: /ws/app?instanceId=his-001) "his-001" → connected ──── WS connected ────────────────► Step 2: Authenticate ────────────────── Send AUTH message with Server validates JWT, OAuth2 Bearer token ────────► associates "his-001" with your identity ◄──────── AUTH_OK Step 3: Query who's online ────────────────── Send GET_CLIENTS ────────► Server checks connected CodingClients ◄──────── GET_CLIENTS_RESPONSE: maria (idle), pedro (coding) Step 4: Send case data ────────────────── Send CODING_SESSION_LAUNCH ────────► Server looks up maria targetUser: maria in connected CodingClients data: ──── CODING_SESSION_LAUNCH ────► CodingClient receives data calls DoCodingWithFormat() ◄──────── CODING_SESSION_CREATED opens WebView2 browser sessionId: sess_abc123 routedTo: maria Maria codes the case... Step 5: Receive result (real-time) ────────────────── Maria clicks "Apply" ◄──────── CODING_APPLIED ◄────── CodingClient sends result sessionId: sess_abc123 data: ``` ### Step 1: Connect to the Server Your app connects to `/ws/app` (NOT `/ws/dll`) with a self-generated `instanceId`: ```javascript // Generate or reuse a unique ID for your app instance const instanceId = 'his-radiology-001'; // or crypto.randomUUID() const ws = new WebSocket( `wss://kodemed.example.com/ws/app?instanceId=${instanceId}` ); ``` ### WebSocket Authentication Requirement WebSocket connections **require** a valid JWT token. The server rejects all unauthenticated WebSocket handshakes when security is enabled (`KODEMED_AUTH_ENABLED=true`, the default). Two methods are supported: 1. **Authorization header** (preferred): Include `Authorization: Bearer ` in the WebSocket handshake 2. **Query parameter** (fallback): Append `?token=` to the WebSocket URL — for clients that cannot set HTTP headers during WebSocket handshake If security is disabled (`KODEMED_AUTH_ENABLED=false`), all connections are allowed. ### Step 2: Authenticate After connecting, send an `AUTH` message with your OAuth2 token: ```json { "type": "AUTH", "instanceId": "his-radiology-001", "timestamp": 1739369600000, "payload": { "token": "" } } ``` The server responds with: ```json { "type": "AUTH_OK", "payload": { "user": "his-system@hospital.ch", "realm": "kodemed" } } ``` > **Note:** Without authentication, the server rejects all subsequent messages. ### Step 3: Discover Connected Coders Query which coders are currently online and available: ```json { "type": "GET_CLIENTS", "instanceId": "his-radiology-001", "timestamp": 1739369600000 } ``` Server responds with: ```json { "type": "GET_CLIENTS_RESPONSE", "payload": { "clients": [ { "user": "maria@hospital.ch", "displayName": "Maria García", "connectedSince": "2026-02-12T08:00:00Z", "status": "idle" }, { "user": "pedro@hospital.ch", "displayName": "Pedro López", "connectedSince": "2026-02-12T09:30:00Z", "status": "coding" } ] } } ``` | `status` | Meaning | |----------|---------| | `idle` | Coder is available, no active session | | `coding` | Coder has a session open (will receive `TARGET_USER_BUSY` if you send another) | ### Step 4: Send Case Data to a Coder ```json { "type": "CODING_SESSION_LAUNCH", "instanceId": "his-radiology-001", "timestamp": 1739369600000, "payload": { "data": "...", "format": "spiges", "targetUserId": "a1b2c3d4-uuid-maria" } } ``` | Field | Required | Description | |-------|----------|-------------| | `data` | Yes | The case data (SpiGes XML, BFS, etc.) | | `format` | Yes | `"spiges"`, `"bfs"`, or `"custom"` | | `targetUserId` | Yes | The coder's user ID (from `GET_CLIENTS_RESPONSE` `user` field) | If successful, the server routes the data to Maria's CodingClient and responds: ```json { "type": "CODING_SESSION_CREATED", "payload": { "sessionId": "sess_abc123", "routedTo": "maria@hospital.ch", "instanceId": "dll-abc-123" } } ``` ### Step 5: Receive Real-Time Events When the coder finishes, you receive the result immediately (no polling): ```json { "type": "CODING_APPLIED", "payload": { "sessionId": "sess_abc123", "data": "modified SpiGes XML" } } ``` Other possible events: | Event | Meaning | |-------|---------| | `CODING_APPLIED` | Coder applied changes. `payload.data` contains modified XML | | `CODING_DISCARDED` | Coder discarded changes. Use your original data | | `CODING_CANCELLED` | Coder closed the window without choosing | | `CODING_TIMEOUT` | Session timed out (coder didn't act in time) | ### Error Events If routing fails, you receive a `CODING_ERROR`: ```json { "type": "CODING_ERROR", "payload": { "error": "TARGET_USER_NOT_CONNECTED", "message": "User maria@hospital.ch is not connected via CodingClient", "availableClients": ["pedro@hospital.ch"] } } ``` | Error code | Meaning | |------------|---------| | `TARGET_USER_NOT_CONNECTED` | The coder is not signed in via CodingClient | | `TARGET_USER_BUSY` | The coder already has an active coding session | | `INVALID_DATA` | The case data could not be parsed | | `UNAUTHORIZED` | Your token is invalid or expired | ### Keep-Alive The server sends `HEARTBEAT` messages periodically. Your app must respond: ```json {"type": "HEARTBEAT", "timestamp": 1739369600000} ``` ```json {"type": "HEARTBEAT_ACK", "instanceId": "his-radiology-001", "timestamp": 1739369600000} ``` If no heartbeat response is received within 60 seconds, the server closes the connection. ### Message Reference (3rd-party app) | Message | Direction | Description | |---------|-----------|-------------| | `AUTH` | App → Server | Authenticate with OAuth2 token | | `AUTH_OK` | Server → App | Authentication successful | | `HEARTBEAT` | Server → App | Keep-alive ping | | `HEARTBEAT_ACK` | App → Server | Keep-alive response | | `GET_CLIENTS` | App → Server | Query connected coders | | `GET_CLIENTS_RESPONSE` | Server → App | List of connected coders | | `CODING_SESSION_LAUNCH` | App → Server | Send case data to a coder | | `CODING_SESSION_CREATED` | Server → App | Session created and routed | | `CODING_APPLIED` | Server → App | Coder applied changes (includes modified data) | | `CODING_DISCARDED` | Server → App | Coder discarded changes | | `CODING_CANCELLED` | Server → App | Coder closed window | | `CODING_ERROR` | Server → App | Error (routing, auth, data, etc.) | | `CODING_TIMEOUT` | Server → App | Session timed out | > **Note:** The CodingClient ↔ Server messages (`UI_SESSION_JOIN`, `SESSION_CLOSE`, > `SESSION_ACTION`, etc.) are internal to KodeMed and not relevant for 3rd-party integration. --- ## 6. Authentication - OAuth2 and OIDC KodeMed uses standard OAuth2/OIDC with PKCE for authentication. It requires an **external** identity provider (IdP) -- KodeMed does not bundle or ship its own IdP. Compatible with: - **Keycloak** - **Azure AD / Entra ID** - **Auth0** - **Okta** - **Any OIDC-compliant provider** ### Configuration | Property | Description | Example | |----------|-------------|---------| | `oauth2Url` | OIDC server base URL | `https://sso.example.com/auth` | | `oauth2Realm` | Realm/tenant name | `kodemed` | | `oauth2ClientId` | Client ID for the DLL | `kodemed-dll` | ### OIDC Endpoints | Endpoint | URL | |----------|-----| | Token | `{oauth2Url}/realms/{realm}/protocol/openid-connect/token` | | Authorization | `{oauth2Url}/realms/{realm}/protocol/openid-connect/auth` | | UserInfo | `{oauth2Url}/realms/{realm}/protocol/openid-connect/userinfo` | | Logout | `{oauth2Url}/realms/{realm}/protocol/openid-connect/logout` | ### Token Flow ``` DLL OIDC Provider Coding UI │ │ │ │── Auth Code + PKCE ─────────>│ │ │<── Authorization Code ───────│ │ │── Exchange Code + Verifier ─>│ │ │<── Access Token + Refresh ───│ │ │ │ │ │── Inject token ─────────────────────────────────────────────>│ │ (window.KODEMED_TOKEN + │ │ localStorage + Auth header) │ │ │ │ │<── TOKEN_REFRESH_REQUEST ───────────────────────────────────│ │── Refresh Token ────────────>│ │ │<── New Access Token ─────────│ │ │── TOKEN_REFRESH ────────────────────────────────────────────>│ ``` ### Client Setup in Your OIDC Provider Configure the following in your **external** OIDC provider (Keycloak, Azure AD, Auth0, Okta, etc.): 1. Create realm `kodemed` 2. Create client `kodemed-dll`: - Client Protocol: `openid-connect` - Access Type: `public` - Standard Flow Enabled: `true` - Valid Redirect URIs: `http://localhost:*` - Web Origins: `*` 3. Create client `kodemed-ui`: - Same as above, for browser-based access 4. Create roles and assign to users as needed ### Group-Based Authorization KodeMed uses OpenID Connect roles for role-based access. Configure these roles in your OIDC provider and assign them to users: | Role | Description | Permissions | |------|-------------|-------------| | `kodemed-admin` | Administrator | Full access: edit administrative data, manage users, approve sessions | | `kodemed-coder` | Coder | Can code cases, edit diagnoses/procedures, view all data | | `kodemed-approve` | Approver | Can approve coding sessions (also granted to `kodemed-admin`) | | `kodemed-viewer` | Viewer | Read-only access to cases and coding results | **How roles are resolved from the token:** The DLL and Coding UI extract roles from the OIDC token in this order: 1. `realm_access.roles` (Keycloak, and compatible providers) 2. `resource_access..roles` (Keycloak client-specific roles) 3. Standard OIDC `roles` claim 4. Auth0-style custom namespace (`https://kodemed.com/roles`) **Frontend helper functions** (available in `authProvider.ts`): | Function | Checks | |----------|--------| | `hasRole(role)` | User has the specified role | | `isAdmin()` | User has `kodemed-admin` | | `canApprove()` | User has `kodemed-approve` or `kodemed-admin` | | `canEdit()` | User is authenticated | | `canView()` | User is authenticated | ### Token Lifecycle & Auto-Refresh KodeMed manages token lifecycle automatically to prevent sessions from dying mid-work. Understanding this is important for all three integration options. #### Two tokens | Token | Typical lifetime | Purpose | |-------|-----------------|---------| | **Access token** | 5 min (configurable in your IdP) | Authenticates every API call and WebSocket message | | **Refresh token** | 30 min (configurable in your IdP) | Used to obtain new access tokens without re-login | #### Auto-refresh mechanism (COM DLL + CodingClient) The DLL runs a **refresh timer** that proactively refreshes the access token **120 seconds before it expires**. This happens transparently - the user never sees it. ``` Timeline (access_token lifetime = 5 min = 300s) ───────────────────────────────────────────────────────────── 0s Login → access_token obtained RefreshTimer scheduled at 180s (300 - 120) 180s ⏰ Timer fires → RefreshTokenAsync() → New access_token obtained (new 300s lifetime) → Token sent to WebView2 via: 1. TOKEN_REFRESH WebSocket message 2. JavaScript injection (window.__KODEMED_UPDATE_TOKEN__) 3. HTTP Authorization header updated → New timer at 180+180=360s 180s If refresh FAILS: → Retry every 15s until success or refresh_token expires → Frontend also requests refresh when < 90s remaining (backup) ~1800s Refresh token expires → user must re-login → AuthStateChanged event fires (isAuthenticated=false) → CodingClient shows "Session expired" balloon ``` #### What happens when tokens expire during work? | Scenario | What happens | HIS action needed | |----------|-------------|-------------------| | **Access token expires** | Auto-refreshed transparently. User doesn't notice | None | | **Refresh fails (network)** | Retried every 15s. Frontend also requests refresh | None (auto-retry) | | **Refresh token expires** | Session dies. `AuthStateChanged` event fires with `isAuthenticated=false` | Show re-login prompt to user | | **User closes laptop** | On resume: WebSocket reconnects, token refreshed via `TokenRefreshCallback` before reconnecting | None (automatic) | | **Server restarts** | WebSocket reconnects, `UI_SESSION_JOIN` re-attaches to existing session | None (automatic) | #### Token lifecycle per integration option **Option A (COM DLL):** Fully automatic. The DLL manages login, refresh, and injection into the WebView2 browser. Your HIS only needs to handle `AuthStateChanged` if you want to show the auth state in your UI. **Option B (REST API):** Your 3rd-party app manages its **own** token separately. The coder's CodingClient manages its token independently. If your token expires: - API calls return `401 Unauthorized` - Refresh your token and retry the request **Option C (WebSocket):** Your 3rd-party app sends an `AUTH` message at connection time. If your token expires during the WebSocket session: - The server sends `AUTH_EXPIRED` → your app must send a new `AUTH` message with a fresh token - If no re-auth within 60s, the server disconnects your WebSocket > **Recommended OIDC provider settings:** > - Access token lifetime: 5 minutes (default) > - Refresh token lifetime: 8 hours (for a full work day) > - SSO session idle: 8 hours > - SSO session max: 12 hours --- ## 7. Data Formats ### SpiGes XML (Default) The Swiss Federal Statistics Office (BFS) SpiGes format. Version 1.5. ```xml ``` ### BFS Pipe-Delimited (.dat) Legacy format. Detected automatically when data starts with `MX|`. ``` MX|100000001|10000001|2026| MB|1|A|1|01|2026|1|01|2026|1|65| MD|1|K3530|3| MD|2|I10|3| MP|1|4701|4|20260115| ``` ### BFS XML BFS XML format transformed to SpiGes via XSLT. Custom XSLT files can be provided. ### Custom Format For proprietary HIS formats, use the plugin system: ```csharp // Register a custom plugin coding.PluginManager.RegisterPlugin(new MyHisFormatPlugin()); coding.SetCustomPlugin("my-his-format"); // Or load from directory coding.LoadPlugins(@"C:\KodeMed\Plugins"); ``` ### FormatType Enum | Value | Name | Description | |-------|------|-------------| | 0 | `SpiGes` | Swiss SpiGes XML format (default) | | 1 | `BFS` | BFS XML or pipe-delimited .dat format | | 2 | `Custom` | Custom format via plugin | --- ## 8. Configuration Reference Configuration can be provided as XML or JSON. The DLL auto-detects the format. ### XML Format ```xml https://kodemed.example.com https://data.kodemed.example.com https://grouper.kodemed.example.com https://kodemed.example.com https://sso.example.com/auth kodemed kodemed-dll 300 XML true 1.5 de true 60 30 60 5 1.0 C:\KodeMed\Plugins my-plugin C:\KodeMed\custom-bfs-to-spiges.xslt C:\KodeMed\custom-spiges-to-bfs.xslt 100000001 ``` ### JSON Format ```json { "serverUrl": "https://kodemed.example.com", "dataServerUrl": "https://data.kodemed.example.com", "grouperServerUrl": "https://grouper.kodemed.example.com", "codingUIUrl": "https://kodemed.example.com", "oauth2Url": "https://sso.example.com/auth", "oauth2Realm": "kodemed", "oauth2ClientId": "kodemed-dll", "timeout": 300, "outputFormat": "JSON", "validateSchema": true, "spiGesVersion": "1.5", "language": "de", "webSocketAutoReconnect": true, "webSocketReconnectIntervalSeconds": 60, "webSocketHeartbeatIntervalSeconds": 30, "sessionExpiryMinutes": 60, "offlineTimeoutMinutes": 5, "zoomFactor": 1.0, "enterpriseId": 100000001 } ``` ### Properties Reference | Property | Type | Default | Description | |----------|------|---------|-------------| | `serverUrl` | string | `http://localhost:8080` | Main server URL | | `dataServerUrl` | string | `http://localhost:8081` | Data catalog server (ICD-10, CHOP) | | `grouperServerUrl` | string | `http://localhost:8082` | DRG grouping server (SwissDRG, TARPSY, ST Reha) | | `codingUIUrl` | string | `http://localhost:3000` | React coding UI base URL | | `webSocketUrl` | string | auto | WebSocket URL (auto-derived from serverUrl) | | `oauth2Url` | string | | OAuth2/OIDC server URL | | `oauth2Realm` | string | `kodemed` | OAuth2 realm/tenant | | `oauth2ClientId` | string | `kodemed-dll` | OAuth2 client ID | | `apiKey` | string | | Legacy API key (use OAuth2 instead) | | `timeout` | int | `300` | Operation timeout in seconds | | `outputFormat` | string | `XML` | Results format: `XML` or `JSON` | | `validateSchema` | bool | `true` | Validate against SpiGes XSD | | `spiGesVersion` | string | `1.5` | SpiGes schema version | | `language` | string | auto | UI language: `de`, `fr`, `it`, `en` | | `verboseLogging` | bool | `true` | Enable detailed logging | | `zoomFactor` | double? | auto | Coding UI zoom (0.25 - 5.0) | | `sessionExpiryMinutes` | int | `60` | Session expiry timeout | | `offlineTimeoutMinutes` | int | `5` | Offline heartbeat timeout | | `retryCount` | int | `3` | Server operation retry count | | `retryDelayMs` | int | `1000` | Retry delay in milliseconds | ### Server Security Configuration These environment variables must be set on the **KodeMed Server** (not the DLL or CodingClient). They control CORS, authentication, encryption at rest, and data protection. | Variable | Default | Description | |----------|---------|-------------| | `KODEMED_AUTH_ENABLED` | `true` | Enable OAuth2/JWT authentication (disable only for development) | | `OIDC_ISSUER_URI` | -- | OAuth2 issuer URI (e.g. `https://sso.example.com/auth/realms/kodemed`) | | `OIDC_JWK_URI` | -- | JSON Web Key Set URI for JWT validation | | `CORS_ALLOWED_ORIGINS` | (empty, **required**) | Comma-separated allowed origins. Wildcard `*` is **blocked** at startup | | `WEBSOCKET_ALLOWED_ORIGINS` | (empty) | WebSocket CORS origins (defaults to `CORS_ALLOWED_ORIGINS` if empty) | | `KODEMED_ENCRYPTION_KEY` | (empty) | Base64-encoded AES-256 key (32 bytes) for encrypting patient data at rest in database | > **CORS is mandatory:** The server will **not start** if `CORS_ALLOWED_ORIGINS` is set to `*`. > You must specify explicit origins (e.g. `https://kodemed.hospital.ch,https://admin.hospital.ch`). > This applies to all three servers (KodeMed.Server, DataServer, GrouperServer). > > **Why wildcard is blocked:** Wildcard CORS (`*`) allows any website to make authenticated requests > to the KodeMed API on behalf of signed-in users, enabling cross-site request forgery (CSRF) and > patient data exfiltration attacks. Explicit origin lists prevent this security risk. > **Encryption at rest (database):** When `KODEMED_ENCRYPTION_KEY` is set, patient data fields > (`originalData`, `resultData`, change history) are encrypted with AES-256-GCM in the database. > Generate a key with: `openssl rand -base64 32`. When empty, data is stored unencrypted. > Existing unencrypted data is read transparently (no migration needed). > **Encryption at rest (browser):** The Coding UI automatically encrypts sensitive medical data > in sessionStorage using Web Crypto API (AES-GCM 256-bit). The encryption key is session-bound, > generated on page load, and cleared on logout. No server configuration needed — this is always active. > **Medical data redaction in logs (DLL):** The KodeMed.dll automatically redacts 25+ medical data > patterns from log files (patient ID, insurance number, diagnosis codes, etc.) to comply with GDPR > Article 5.1.f (data minimization). Controlled by `AllowMedicalDataInLogs` flag in DLL configuration > (default: `true` for debugging, should be `false` in production). Sensitive tokens (JWT, Bearer) are > always redacted regardless of this flag. --- ## 9. CodingClient Setup The KodeMed Coding Client is a portable system tray application that: - Receives coding sessions from the server via WebSocket - Allows users to open sessions from local files - Handles OAuth2 authentication - Auto-starts on Windows login - Works on Citrix/VDI (user-level, no admin) ### Installation #### Method 1: MSI Installer (recommended) The `KodeMed.msi` package handles installation, configuration, COM registration, and autostart setup. No admin rights required for per-user install. Download from: [portal.kodemed.com](https://portal.kodemed.com) **Interactive Install** — double-click `KodeMed.msi` or run: ```cmd msiexec /i KodeMed.msi ``` The installer prompts for server URL, language, and autostart preference. **Silent Install** — for GPO/SCCM/Intune deployment: ```cmd msiexec /i KodeMed.msi /quiet /norestart SERVERURL="https://kodemed.hospital.ch" LANGUAGE="de" AUTOSTART=1 ``` **Per-machine Install** — for Citrix/Terminal Server (requires admin): ```cmd msiexec /i KodeMed.msi /quiet /norestart ALLUSERS=1 SERVERURL="https://kodemed.hospital.ch" ``` **MSI Properties:** | Property | Description | Default | |----------|-------------|---------| | `SERVERURL` | KodeMed server URL | Prompted (interactive) or app prompts on first launch | | `LANGUAGE` | UI language (de/fr/it/en) | System culture | | `AUTOSTART` | Enable Windows autostart (1/0) | 1 | | `ALLUSERS` | Per-machine install (1) or per-user (empty) | Per-user | | `LAUNCHAPP` | Launch app after install (1/0) | 1 | **Uninstall:** ```cmd msiexec /x KodeMed.msi /quiet ``` Or via Windows Settings > Apps > KodeMed CodingClient > Uninstall. #### Method 2: Manual (portable) 1. Copy the `KodeMed.CodingClient` folder anywhere on the user's machine 2. Run `KodeMed.CodingClient.exe` 3. First run automatically: - Sets environment variables (`KODEMED_HOME`, `KODEMED_DLL_PATH`, `KODEMED_EXE_PATH`) - Registers the COM DLL (user-level, HKCU) - Creates marker file `.kodemed-installed` ### Configuration The CodingClient only needs the **server URL**. All other settings (OAuth2, CodingUI URL, etc.) are fetched automatically from the server's `/api/v1/config` endpoint on startup. **Option 1: Command-line argument (recommended for deployment)** ```bash # First run with server URL - auto-configures and saves locally KodeMed.CodingClient.exe --server-url https://kodemed.example.com # Run multiple instances for different environments KodeMed.CodingClient.exe --server-url https://test.kodemed.example.com KodeMed.CodingClient.exe --server-url https://prod.kodemed.example.com ``` The `--server-url` parameter overwrites the local config file. After the first run, you can start the EXE without parameters and it will use the saved config. **Option 2: Edit config file** Edit `kodemed-client-config.json` next to the EXE or in `%KODEMED_HOME%`: ```json { "serverUrl": "https://kodemed.example.com", "language": "de", "webSocketAutoReconnect": true, "webSocketReconnectIntervalSeconds": 60, "webSocketHeartbeatIntervalSeconds": 30 } ``` > **Note:** The `language` field sets the UI language for the CodingClient. Valid values > are `de` (German), `fr` (French), `it` (Italian), and `en` (English). The language can > also be changed at runtime via the tray menu Language submenu. After the first connection, the CodingClient fetches OAuth2 and UI URLs from the server and saves the complete config locally. You can then customize any values. **Option 3: Tray menu** Use the **Server Settings** menu in the tray icon to enter the server URL. ### Startup Behavior 1. Load local config (or apply `--server-url` if provided) 2. Connect to server and fetch remaining config (`/api/v1/config`) 3. Save complete config locally for future starts / customization 4. If OAuth2 is configured: **auto-trigger sign-in** (login popup appears) 5. Connect WebSocket and listen for coding sessions The tray tooltip always shows which server the client is connected to (hostname). ### Environment Variables | Variable | Description | Example | |----------|-------------|---------| | `KODEMED_HOME` | Working directory | `C:\Users\joe\KodeMed\` | | `KODEMED_DLL_PATH` | Full path to KodeMed.dll | `C:\Users\joe\KodeMed\KodeMed.dll` | | `KODEMED_EXE_PATH` | Full path to CodingClient.exe | `C:\Users\joe\KodeMed\KodeMed.CodingClient.exe` | ### Citrix / VDI Deployment - **Self-contained EXE**: No .NET runtime dependency - **User-level only**: All registry writes to HKCU, env vars to User scope - **No admin rights required**: COM registration via HKCU fallback - **Portable**: Copy folder anywhere, run EXE, auto-setup on first launch - **Non-persistent sessions**: Setup re-runs silently if marker file lost ### Tray Menu ``` KodeMed Coding Client v2026.3 (username) ───────────────────────────── Sign In Sign Out ───────────────────────────── Open Session from File... > SpiGes (XML) BFS (DAT/XML) Custom... ───────────────────────────── Language > DE (Deutsch) FR (Français) IT (Italiano) EN (English) ───────────────────────────── Server Settings... Auto-start on Login ✓ ───────────────────────────── Setup / Repair ───────────────────────────── Exit ``` --- ## 10. Post-Coding Webhook The CodingClient can send coding results to an external endpoint (e.g., your HIS) via an HTTP POST after each completed coding session. This is a **fire-and-forget** mechanism that does not block the coder. ### How It Works ``` Coder completes session → CodingClient fires webhook → HIS receives results (async, does not block) ``` 1. The coder completes a coding session (Applied, Discarded, etc.) 2. The CodingClient checks if the webhook is enabled and the event matches 3. If yes, it POSTs a JSON payload to the configured URL in the background 4. Retries with exponential backoff on failure (1s → 2s → 4s → ... up to 60s) 5. Does not retry 4xx client errors (except 429 Too Many Requests) ### Configuration Webhook configuration is split between the **server** (centralized administration) and the **client** (local overrides and auth credentials). **Server-side** (environment variables in `kodemed.env`): | Variable | Default | Description | |----------|---------|-------------| | `KODEMED_HOOK_ENABLED` | `false` | Enable/disable the webhook globally | | `KODEMED_HOOK_URL` | (empty) | Target URL for the HTTP POST | | `KODEMED_HOOK_AUTH_TYPE` | `none` | Authentication type: `none`, `bearer`, `header` | | `KODEMED_HOOK_TIMEOUT_SECONDS` | `30` | HTTP request timeout (max 300) | | `KODEMED_HOOK_RETRY_COUNT` | `3` | Number of retries on failure (max 10) | | `KODEMED_HOOK_INCLUDE_RESULT_DATA` | `false` | Include the result XML/JSON in the payload (data minimization: opt-in) | | `KODEMED_HOOK_INCLUDE_ORIGINAL_DATA` | `false` | Include the original (unmodified) data | | `KODEMED_HOOK_INCLUDE_GROUPER_RESULTS` | `false` | Include per-case DRG/grouper results (data minimization: opt-in) | | `KODEMED_HOOK_EVENTS` | `applied` | Comma-separated events to fire on (case-insensitive) | Valid events: `applied`, `discarded`, `cancelled`, `timeout`, `error` **Client-side** (local overrides in `kodemed-client-config.json`): Auth credentials are **never sent by the server** for security reasons. They must be configured locally on each CodingClient machine. The MSI installer writes the server URL to `kodemed-client-config.json`; webhook auth must be configured manually in the config file. ```json { "serverUrl": "https://kodemed.example.com", "hook": { "enabled": true, "url": "https://his.hospital.ch/api/coding-results", "authType": "bearer", "authToken": "sk-hospital-api-key-123", "events": "applied,discarded" } } ``` Local values override server values. Only set the fields you want to override — unset fields fall back to the server configuration. | Local Override | Type | Description | |----------------|------|-------------| | `hook.enabled` | bool | Override server enable/disable | | `hook.url` | string | Override target URL | | `hook.authType` | string | `none`, `bearer`, or `header` | | `hook.authToken` | string | Bearer token (for `authType: "bearer"`) | | `hook.authHeaderName` | string | Custom header name (for `authType: "header"`) | | `hook.authHeaderValue` | string | Custom header value (for `authType: "header"`) | | `hook.timeoutSeconds` | int | Override timeout | | `hook.retryCount` | int | Override retry count | | `hook.includeResultData` | bool | Override result data inclusion | | `hook.includeOriginalData` | bool | Override original data inclusion | | `hook.includeGrouperResults` | bool | Override grouper results inclusion | | `hook.events` | string | Override event filter | ### Authentication Examples **Bearer Token:** ```json { "hook": { "authType": "bearer", "authToken": "sk-hospital-api-key-123" } } ``` Sends: `Authorization: Bearer sk-hospital-api-key-123` **Custom Header:** ```json { "hook": { "authType": "header", "authHeaderName": "X-API-Key", "authHeaderValue": "my-secret-key" } } ``` Sends: `X-API-Key: my-secret-key` ### Webhook Payload The POST body is JSON with `Content-Type: application/json`: ```json { "eventType": "coding_session_completed", "timestamp": "2026-02-12T14:30:00Z", "sessionId": "sess_abc123", "codingAction": "Applied", "applied": true, "discarded": false, "username": "dr.muller", "sourceFormat": "SpiGes", "durationMs": 45200, "casesProcessed": 3, "diagnosesCount": 12, "treatmentsCount": 7, "success": true, "resultData": "...", "originalData": "...", "grouperResults": [ { "fallId": 123, "drgCode": "B60B", "costWeight": 1.234, "pccl": 2, "los": 5, "tariff": "SWISSDRG", "groupingSuccess": true } ] } ``` | Field | Type | Always | Description | |-------|------|--------|-------------| | `eventType` | string | Yes | Always `"coding_session_completed"` | | `timestamp` | string | Yes | ISO 8601 UTC timestamp | | `sessionId` | string | Yes | Coding session ID | | `codingAction` | string | Yes | `Applied`, `Discarded`, `Cancelled`, `Timeout`, `Error` | | `applied` | bool | Yes | Whether user applied changes | | `discarded` | bool | Yes | Whether user explicitly discarded | | `username` | string | Yes | Authenticated coder's username (null if anonymous) | | `sourceFormat` | string | Yes | Input format: `SpiGes`, `BFS`, `Custom` | | `durationMs` | long | Yes | Session duration in milliseconds | | `casesProcessed` | int | Yes | Number of cases in the data | | `diagnosesCount` | int | Yes | Total diagnoses across all cases | | `treatmentsCount` | int | Yes | Total treatments across all cases | | `success` | bool | Yes | Whether processing was successful | | `errorMessage` | string | No | Error details (only when `success: false`) | | `resultData` | string | If enabled | Result XML/JSON (controlled by `includeResultData`) | | `originalData` | string | If enabled | Original data (controlled by `includeOriginalData`) | | `grouperResults` | array | If enabled | Per-case DRG results (controlled by `includeGrouperResults`) | ### Retry Behavior | Scenario | Behavior | |----------|----------| | HTTP 2xx | Success — no retry | | HTTP 4xx (except 429) | Client error — **no retry** (fix your endpoint) | | HTTP 429 | Rate limited — retry with backoff | | HTTP 5xx | Server error — retry with backoff | | Timeout | Retry with backoff | | Network error | Retry with backoff | Backoff: 1s → 2s → 4s → 8s → ... capped at 60s. Default 3 retries (4 total attempts). ### HIS Integration Example (C#) Minimal ASP.NET endpoint to receive webhook results: ```csharp [ApiController] [Route("api/coding-results")] public class CodingResultsController : ControllerBase { [HttpPost] public IActionResult ReceiveCodingResult([FromBody] JsonElement payload) { var sessionId = payload.GetProperty("sessionId").GetString(); var action = payload.GetProperty("codingAction").GetString(); var applied = payload.GetProperty("applied").GetBoolean(); if (applied) { var resultData = payload.GetProperty("resultData").GetString(); // Process the modified SpiGes XML... } return Ok(); } } ``` --- ## 11. API Reference ### COM Interface Methods (IKodeMed) These methods are exposed via the formal COM interface. Accessible from VB.NET, VBA, Delphi, and any COM-compatible language via `CreateObject("KodeMed.Coding")`. | Method | Signature | Description | |--------|-----------|-------------| | `SendConfig` | `bool SendConfig(string configData)` | Load XML or JSON configuration (auto-detected) | | `DoCoding` | `bool DoCoding(string data, FormatType format)` | Process headless: validation + grouping, no browser UI | | `DoCodingWithFormat` | `bool DoCodingWithFormat(string data, FormatType format)` | **Main method**: authenticates via OAuth2, opens embedded WebView2 browser, user codes interactively | | `GetResults` | `string GetResults()` | Get results as XML or JSON (based on `outputFormat` config) | | `GetConfigFromServer` | `string GetConfigFromServer()` | Fetch config from the KodeMed server API | | `SetConfigFromServer` | `bool SetConfigFromServer()` | Fetch + apply server config in one call | | `DisposeClient` | `void DisposeClient()` | Release all resources (WebSocket, browser, auth) | ### COM Interface Properties (IKodeMed) | Property | Type | Description | |----------|------|-------------| | `Status` | `ProcessingStatus` | Current processing status | | `LastError` | `string` | Last error message (empty if no error) | | `DetectedConfigFormat` | `ConfigFormatType` | Format detected from last `SendConfig()` call | ### Extended Methods (Coding class) These methods are available when referencing `KodeMed.dll` directly from .NET (C#, VB.NET). They extend beyond the formal COM interface with additional functionality. | Method | Signature | Description | |--------|-----------|-------------| | `ConnectToServer` | `bool ConnectToServer(string serverUrl)` | Connect WebSocket to KodeMed Server | | `ConnectToServer` | `bool ConnectToServer(string serverUrl, string authToken)` | Connect WebSocket with explicit JWT token | | `ConnectToServer` | `bool ConnectToServer(ServerConnectionConfig config)` | Connect WebSocket with full config (reconnect, heartbeat, etc.) | | `DisconnectFromServer` | `void DisconnectFromServer()` | Disconnect WebSocket | | `Logout` | `void Logout()` | Clear OAuth2 tokens and disconnect | | `GetAccessToken` | `string GetAccessToken()` | Get current OAuth2 access token (refreshes if expired) | | `CancelProcessing` | `void CancelProcessing()` | Cancel an ongoing coding session | | `GetResultData` | `string GetResultData()` | Get the modified SpiGes XML from the last session | | `GetOriginalData` | `string GetOriginalData()` | Get the original (unmodified) case data | | `SendServerMessage` | `bool SendServerMessage(string type, string payload)` | Send a custom WebSocket message to the server | | `LoadPlugins` | `void LoadPlugins(string directory)` | Load format plugins from a directory | | `SetCustomPlugin` | `void SetCustomPlugin(string pluginId, string config)` | Activate a custom format plugin | | `SetBfsXslt` | `void SetBfsXslt(string bfsToSpiGes, string spiGesToBfs)` | Set custom BFS↔SpiGes XSLT files | ### Extended Properties (Coding class) | Property | Type | Description | |----------|------|-------------| | `IsAuthenticated` | `bool` | Whether user is authenticated via OAuth2 | | `CurrentUsername` | `string` | Authenticated user's display name | | `HasOAuth2Config` | `bool` | Whether OAuth2 is configured (oauth2Url is set) | | `LastCodingAction` | `CodingAction` | Result of the last coding session | | `WasDiscarded` | `bool` | Whether last session was explicitly discarded by user | | `IsServerConnected` | `bool` | Whether WebSocket is connected to the server | | `ServerConnectionState` | `ConnectionState` | Detailed WebSocket connection state | | `CurrentSessionId` | `string` | Active coding session ID (null if none) | | `InstanceId` | `string` | Unique DLL instance identifier (UUID, auto-generated) | | `AuthService` | `AuthenticationService` | Direct access to the OAuth2 authentication service | | `ServerConnection` | `IServerConnection` | Direct access to the WebSocket connection | | `LastResults` | `CodingResults` | Full results object from the last session | | `HookConfiguration` | `HookConfig` | Webhook hook config fetched from server (read-only) | ### ProcessingStatus Enum | Value | Name | Description | |-------|------|-------------| | 0 | `NotInitialized` | No configuration loaded | | 1 | `Configured` | Config loaded, ready to process | | 2 | `DataLoaded` | Case data loaded | | 3 | `Processing` | Processing in progress | | 4 | `Completed` | Processing completed | | 5 | `Error` | Error occurred | | 6 | `Disposed` | Client disposed | ### CodingAction Enum | Value | Name | Description | |-------|------|-------------| | 0 | `Pending` | No action taken yet | | 1 | `Applied` | User applied changes (modified data available in `ResultData`) | | 2 | `Discarded` | User explicitly discarded changes (original data returned) | | 3 | `Cancelled` | User closed window without choosing Apply or Discard | | 4 | `Timeout` | Session timed out (no user action within timeout period) | | 5 | `Error` | Error occurred during processing | > **For the HIS:** If `CodingAction` is `Applied`, use `ResultData` (modified data). > For **all other values** (`Discarded`, `Cancelled`, `Timeout`, `Error`), use the > original data you sent. The `WasDiscarded` property only returns `true` for > `Discarded` (not for `Cancelled`/`Timeout`), so check `CodingAction` directly. ### ConnectionState Enum | Value | Name | Description | |-------|------|-------------| | 0 | `Disconnected` | Not connected to server | | 1 | `Connecting` | Connection attempt in progress | | 2 | `Connected` | WebSocket connected and operational | | 3 | `Reconnecting` | Lost connection, attempting to reconnect (automatic) | | 4 | `Error` | Connection error occurred | | 5 | `Terminated` | Max reconnection attempts reached, gave up | ### REST API Endpoints Summary #### Coding Sessions | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/coding/clients` | List connected CodingClient coders (userId, instanceId, status) | | `POST` | `/api/v1/coding/session` | Create coding session (use `targetUserId` to route to a coder) | | `GET` | `/api/v1/coding/session/{id}` | Get session metadata | | `GET` | `/api/v1/coding/session/{id}/data` | Get session input data (raw XML/JSON) | | `POST` | `/api/v1/coding/session/{id}/complete` | Complete session with result data and action | | `POST` | `/api/v1/coding/session/{id}/cancel` | Cancel session | | `POST` | `/api/v1/coding/session/{id}/save` | Save current progress without completing | | `POST` | `/api/v1/coding/session/{id}/change` | Record a case change for audit trail | | `GET` | `/api/v1/coding/session/{id}/result` | Get result metadata after completion | | `GET` | `/api/v1/coding/session/{id}/result/data` | Get result data (raw XML/JSON) | | `GET` | `/api/v1/coding/active` | Check if current user has an active session | | `GET` | `/api/v1/coding/sessions` | Get recent sessions (last N days) | | `GET` | `/api/v1/coding/sessions/{id}/data` | Get historical session data (read-only) | | `GET` | `/api/v1/coding/sessions/{id}/status` | Get session online/offline status | | `GET` | `/api/v1/coding/history` | Get session history for current user | | `GET` | `/api/v1/coding/last-session` | Get last completed session (read-only) | | `GET` | `/api/v1/coding/last-session/data` | Get last session data (read-only) | | `GET` | `/api/v1/coding/health` | Coding service health check | #### Undo History (per-case) | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo/record` | Record new undo action | | `GET` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo` | Get case undo history | | `POST` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo` | Undo last action | | `POST` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo/{actionId}` | Undo specific action by ID | | `DELETE` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo` | Clear case undo history | | `GET` | `/api/v1/coding/session/{sessionId}/case/{caseId}/undo/count` | Get undo count for case | #### Layout Management | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/v1/layouts` | Create and share a new layout | | `GET` | `/api/v1/layouts/{layoutId}` | Get layout by ID | | `PUT` | `/api/v1/layouts/{layoutId}` | Update shared layout (owner only) | | `DELETE` | `/api/v1/layouts/{layoutId}` | Delete shared layout (owner only) | | `GET` | `/api/v1/layouts/code/{shareCode}` | Get layout by share code | | `GET` | `/api/v1/layouts/public` | Get public layouts (gallery) | | `GET` | `/api/v1/layouts/my-shared` | Get layouts shared by current user | | `POST` | `/api/v1/layouts/{layoutId}/regenerate-code` | Regenerate share code | | `POST` | `/api/v1/layouts/{layoutId}/import` | Record layout import (statistics) | | `PUT` | `/api/v1/layouts/sync` | Sync local layouts to server (backup) | | `GET` | `/api/v1/layouts/sync` | Get synced layouts from server (restore) | #### Audit Trail | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/audit/status` | Check if audit is enabled | | `GET` | `/api/v1/audit/session/{sessionId}` | Get session audit trail (requires ADMIN/AUDITOR role) | | `GET` | `/api/v1/audit/case/{caseId}` | Get case audit trail across sessions (requires ADMIN/AUDITOR role) | #### Instance Management | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/instances` | Get all connected DLL instances | | `GET` | `/api/v1/instances/count` | Get connected instance count | | `GET` | `/api/v1/instances/{instanceId}` | Get specific instance details | | `POST` | `/api/v1/instances/{instanceId}/message` | Send message to specific instance | | `POST` | `/api/v1/instances/broadcast` | Broadcast message to all instances | #### Configuration | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/config` | Get server configuration | --- ### Detailed API Documentation #### Undo History API Manages per-case undo/redo functionality. Each case in a session has its own independent undo stack. **Base Path:** `/api/v1/coding/session/{sessionId}/case/{caseId}/undo` **Authentication:** Required (OAuth2 Bearer token) **POST `/api/v1/coding/session/{sessionId}/case/{caseId}/undo/record`** - Record a new undo action Request Body: ```json { "caseId": "fall_001", "actionType": "CODE_ADDED|CODE_REMOVED|CODE_MODIFIED|FIELD_CHANGED", "code": "I10.00", "description": "Added hypertension diagnosis", "snapshotBefore": "{ ... }", "snapshotAfter": "{ ... }" } ``` Response: `201 Created` ```json { "recorded": true, "actionId": "uuid", "timestamp": "2026-02-16T10:00:00Z" } ``` **GET `/api/v1/coding/session/{sessionId}/case/{caseId}/undo`** - Get undo history Response: `200 OK` ```json { "caseId": "fall_001", "actions": [ { "id": "uuid", "actionType": "CODE_ADDED", "code": "I10.00", "timestamp": "2026-02-16T10:00:00Z", "canUndo": true } ], "count": 5, "canUndo": true } ``` **POST `/api/v1/coding/session/{sessionId}/case/{caseId}/undo`** - Undo last action Response: `200 OK` ```json { "undone": true, "actionId": "uuid", "restoredState": "{ ... }" } ``` --- #### Layout Management API Manages shared and synced coding UI layouts. Allows users to share custom column configurations and sync layouts across devices. **Base Path:** `/api/v1/layouts` **Authentication:** Required (OAuth2 Bearer token) **POST `/api/v1/layouts`** - Create and share a new layout Request Body: ```json { "name": "My Custom Layout", "description": "Optimized for cardiology coding", "layout": { "columns": [...], "filters": {...} }, "visibility": "PUBLIC|PRIVATE|UNLISTED", "tags": ["cardiology", "drg"] } ``` Response: `200 OK` ```json { "layoutId": "uuid", "shareCode": "ABC123", "shareUrl": "https://kodemed.example.com/layouts/code/ABC123", "visibility": "PUBLIC" } ``` **GET `/api/v1/layouts/public?query=cardiology&page=0&pageSize=20`** - Get public layouts Response: `200 OK` ```json { "layouts": [ { "layoutId": "uuid", "name": "...", "owner": "...", "importCount": 42, "tags": ["cardiology"] } ], "total": 100, "page": 0, "pageSize": 20 } ``` --- #### Audit API Access audit trail information for tracking changes to coded cases. Only available when persistence and audit are enabled. **Base Path:** `/api/v1/audit` **Authentication:** Required (OAuth2 Bearer token + role `ADMIN`, `AUDITOR`, or scope `audit:read`) **GET `/api/v1/audit/status`** - Check if audit is enabled Response: `200 OK` ```json { "persistenceEnabled": true, "auditEnabled": true } ``` **GET `/api/v1/audit/session/{sessionId}`** - Get session audit trail Response: `200 OK` ```json [ { "id": 1, "sessionId": "sess_abc123", "caseId": "fall_001", "changeType": "CODE_ADDED|CODE_REMOVED|CODE_MODIFIED|FIELD_CHANGED|OTHER", "fieldName": "hauptDiagnose", "codeValue": "I10.00", "oldValue": "", "newValue": "I10.00", "userId": "user@example.com", "userName": "Dr. Smith", "timestamp": "2026-02-16T10:00:00Z", "clientIp": "192.168.1.100", "details": "Added hypertension diagnosis" } ] ``` Error Responses: - `404 Not Found` - Session not found or audit disabled --- #### Instance Management API Manages DLL instance connections and messaging. Allows monitoring and controlling connected CodingClient instances. **Base Path:** `/api/v1/instances` **Authentication:** Required (OAuth2 Bearer token) **GET `/api/v1/instances`** - Get all connected instances Response: `200 OK` ```json [ { "instanceId": "abcd****efgh", "status": "CONNECTED|DISCONNECTED|RECONNECTING", "connectedAt": "2026-02-16T09:00:00Z", "lastActivity": "2026-02-16T10:00:00Z", "disconnectedAt": null, "reconnectAttempts": 0 } ] ``` **Note:** Instance IDs are masked for privacy (first 4 + last 4 characters) **POST `/api/v1/instances/{instanceId}/message`** - Send message to instance Request Body: ```json { "type": "CUSTOM_MESSAGE", "payload": {...}, "timestamp": 1708077600000 } ``` Response: `200 OK` --- #### Additional Coding Session Endpoints **POST `/api/v1/coding/session/{sessionId}/save`** - Save progress without completing Request Body: ```json { "data": "", "modifiedCount": 3 } ``` Response: `200 OK` ```json { "sessionId": "sess_abc123", "status": "SAVED", "persistenceEnabled": true } ``` **GET `/api/v1/coding/last-session`** - Get last completed session for current user (read-only) Response: `200 OK` ```json { "sessionId": "sess_abc123", "format": "spiges", "status": "COMPLETED", "action": "APPLIED", "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T10:00:00Z", "hasData": true, "readOnly": true } ``` Error Responses: - `401 Unauthorized` - Authentication required - `404 Not Found` - No completed sessions found - `503 Service Unavailable` - Persistence is disabled --- #### Error Codes All APIs follow standard HTTP status codes: | Code | Meaning | When | |------|---------|------| | `200 OK` | Success | Request completed successfully | | `201 Created` | Resource created | POST requests that create new resources | | `204 No Content` | Success with no body | DELETE requests | | `400 Bad Request` | Invalid request | Malformed JSON, missing required fields | | `401 Unauthorized` | Authentication required | Missing or invalid Bearer token | | `403 Forbidden` | Access denied | User lacks required role/scope | | `404 Not Found` | Resource not found | Invalid session/layout/case ID | | `409 Conflict` | Resource conflict | Session already exists, concurrent modification | | `500 Internal Server Error` | Server error | Unexpected server-side error | | `503 Service Unavailable` | Service disabled | Feature disabled (e.g., persistence, audit) | --- #### Authentication All REST API endpoints require OAuth2 authentication via Bearer token: ```http GET /api/v1/layouts/public HTTP/1.1 Host: kodemed.example.com Authorization: Bearer eyJhbGciOiJSUzI1NiIs... ``` **Role Requirements:** - **Audit API**: Requires `ADMIN`, `AUDITOR` role, or `audit:read` scope - **Instance API**: Requires authentication (no specific role) - **Layout API**: Requires authentication (no specific role) - **Undo History API**: Requires authentication (no specific role) - **Session History APIs**: Only returns sessions belonging to the authenticated user For details on obtaining tokens, see [Section 6 - Authentication](#6-authentication---oauth2-and-oidc). ### WebSocket Endpoints | Endpoint | Client Type | Description | |----------|-------------|-------------| | `/ws/dll` | CodingClient | For CodingClient tray app connections (coders) | | `/ws/app` | 3rd-party app | For 3rd-party applications sending sessions | ### Results Structure The `GetResults()` method returns XML or JSON (based on `outputFormat` config): ```xml 2026-02-12T10:00:00 2026-02-12T10:05:00 300000 1 5 3 true sess_abc123 Applied true false 1 I68A Non-specific procedures on the musculoskeletal system K3530 1.234 2 5 SWISSDRG true ``` --- ## 12. Troubleshooting ### Common Issues | Issue | Cause | Solution | |-------|-------|----------| | `WebView2 not found` | Edge WebView2 Runtime missing | Install from https://developer.microsoft.com/en-us/microsoft-edge/webview2/ | | `SendConfig returns false` | Invalid XML/JSON | Check `LastError` for details | | `OAuth2 redirect fails` | Wrong redirect URI in IdP | Add `http://localhost:*` to Valid Redirect URIs | | `WebSocket won't connect` | Wrong server URL or firewall | Check `serverUrl` and network access | | `Token expired mid-session` | Refresh token also expired | Increase session timeout or token validity | | `COM registration failed` | No write access to HKCU | Run CodingClient or repair setup | | Server fails to start with CORS error | `CORS_ALLOWED_ORIGINS` is `*` or empty | Set explicit origins: `CORS_ALLOWED_ORIGINS=https://kodemed.hospital.ch` | | CORS preflight rejected | Origin not in allowed list | Add your origin to `CORS_ALLOWED_ORIGINS` (comma-separated) | | WebSocket handshake rejected | Missing JWT token | Add `Authorization: Bearer ` header or `?token=` query param | | Encrypted data unreadable (database) | Wrong encryption key | Use the same `KODEMED_ENCRYPTION_KEY` that was used to encrypt. Data encrypted with a different key cannot be decrypted | | Session lost after page refresh | SessionStorage encryption key lost | By design for security — sessionStorage encryption key is regenerated on page load and not persisted. Users should complete sessions before closing the browser | | Webhook not firing | Hook disabled or event not in list | Check `KODEMED_HOOK_ENABLED=true` and `KODEMED_HOOK_EVENTS` includes your action | | Webhook 401/403 | Auth credentials missing or wrong | Configure `hook.authToken` or `hook.authHeaderName/Value` in `kodemed-client-config.json` | | Webhook retrying endlessly | Target server down | Check CodingClient logs in `%TEMP%\KodeMed\`; max retries is capped (default 3) | ### Logging DLL logs are written to `%TEMP%\KodeMed\` with daily rotation. Enable verbose logging: ```json { "verboseLogging": true } ``` ### Support - Website: https://kodemed.com - Email: info@kodemed.com - Issues: Contact your KodeMed integration partner --- ## 13. Cloud & Orchestration ### Container Deployment All KodeMed server components are delivered as OCI-compliant Docker images, ready for Kubernetes, OpenShift, or Docker Compose deployments. ### Deployment Topology ``` +---------- Harbor Registry ----------+ +------ Kubernetes / OpenShift Cluster ------+ | kodemed/kodemed-server:latest | pull | Ingress / Route | | kodemed/kodemed-dataserver:latest |------->| +-- kodemed-server [HPA] :8080 | | kodemed/kodemed-grouper-server:latest| | +-- kodemed-dataserver [HPA] :8081 | | kodemed/kodemed-ui:latest | | +-- kodemed-grouper :8082 | | | | +-- kodemed-ui :3000 | | Trivy + Cosign/Notary | | PostgreSQL (StatefulSet / external) | +-------------------------------------+ +--------------------------------------------+ ``` ### Kubernetes / OpenShift Features - **Helm charts** or plain K8s manifests for deployment - **HPA** (Horizontal Pod Autoscaling) for Server & DataServer - **Health probes**: `/actuator/health` for liveness and readiness - **OpenShift**: Native support via Routes, DeploymentConfigs & integrated registry ### Harbor Registry - Private image registry with role-based access control - **Trivy** vulnerability scanning on every push - **Cosign / Notary** image signing for supply chain security - Replication policies for multi-site hospital deployments --- *KodeMed GmbH Integration Guide v2026.2 — Updated 2026-02-24 (Cloud & Orchestration)*