# KodeMed HIS REST Integration — Specification **Version:** 0.3 (Aligned with OpenAPI — `CodingSessionRequest` / `HisWebhookPayload`) **Status:** Proposal — pending HIS partner validation **Date:** 2026-05-12 **Audience:** HIS partners integrating with KodeMed via REST + Webhook --- ## 1. Purpose This document specifies a REST integration flow between KodeMed and a partner Hospital Information System (HIS). The flow is designed to be **generic** and supports multiple HIS partners through configuration only — no per-partner code branches. ## 2. Context KodeMed currently offers three integration flows: DLL/COM, WebSocket/CodingClient, REST polling. None fits a web-based HIS partner that wants to: - Start a coding session with clinical data via REST - Open the KodeMed UI in an iframe or a new window - Receive results via webhook when the coder confirms or discards - Use their own Identity Provider (Keycloak) for authentication This document defines a fourth flow that addresses these requirements. ## 3. Flow Architecture ``` ┌────────────┐ ┌─────────────┐ │ HIS │ │ KodeMed │ └─────┬──────┘ └──────┬──────┘ │ 1. POST /api/v1/coding/session │ │ Authorization: Bearer │ │ Body: { data, format:"spiges", source:"API", instanceId } │ │ ─────────────────────────────────────────────────────────────> │ │ │ │ a. Validate token vs configured HIS Keycloak issuer │ │ b. Extract user identity + roles (viewer / coder) │ │ c. Create CodingSession │ │ │ │ 2. 201 Created │ │ { sessionId, redirectUrl, expiresAt } │ │ <───────────────────────────────────────────────────────────── │ │ │ │ 3. HIS opens redirectUrl in iframe or new window │ │ ───────────────────────────────────────────────────────> │ │ Coder uses normal KodeMed UI (full functionality) │ │ Coder presses Apply or Discard │ │ │ │ 4. POST │ │ Authorization: HMAC-SHA256 t=,v1= │ │ (only when IntegrationProfile.webhookSigning=HMAC_SHA256) │ │ Idempotency-Key: : │ │ Body: { event_type, occurred_at, session_id, │ │ instance_id, spiges{ent_id,burnr,fall_id}, │ │ result_data } │ │ <───────────────────────────────────────────────────────────── │ │ │ │ 5. 200 OK │ │ ─────────────────────────────────────────────────────────────> │ ``` ## 4. Authentication ### 4.1 Identity Provider The HIS partner provides a **Keycloak** IDP. KodeMed must be configured to accept JWT tokens issued by that Keycloak realm (specific issuer URI). KodeMed supports **multiple issuers in parallel** — every partner uses its own realm (planned for general use under **#491-D**; today every partner shares the existing KodeMed Keycloak). ### 4.2 Required token claims | Claim | Type | Required | Description | |---|---|---|---| | `sub` | string | yes | Unique user identifier | | `email` | string | yes | User email (matched against KodeMed users) | | `realm_access.roles[]` or `groups[]` | array | yes | Roles claim (claim path is configurable per IntegrationProfile: `oidcRolesPrimaryClaimPath` / `oidcRolesFallbackClaimPath`). Each role value is normalised to `ROLE_`; `kodemed-`-prefixed values ALSO emit a stripped variant — see [Roles & Permissions](KodeMed_Roles_and_Permissions.md) for the canonical table. | | `exp`, `iat`, `iss` | standard | yes | Standard JWT claims | > **Status:** the multi-issuer wiring (`SecurityConfig` accepting tokens from each `IntegrationProfile.oidcIssuerUri`) is tracked under **#491-D**. Until that ships, the production server only validates tokens against its single configured issuer. ### 4.3 Roles — *Planned (#491-D)* The role-to-capability mapping below is the **target contract**, tracked under **#491-D**. The current server does not enforce a viewer / coder split on the HIS REST entry point; any authenticated principal that the existing `CodingSessionController` accepts may open and complete sessions. Partners should provision realm roles in their IDP today so the switch is a config change later, not a re-deploy. Canonical role names are `kodemed-`-prefixed; partner IdPs that publish the plain form (`coder`, `viewer`) are also accepted by the dual-emit converter. See [Roles & Permissions](KodeMed_Roles_and_Permissions.md) for the full table. | Role (canonical) | Plain alternative | Capabilities (target) | |---|---|---| | `kodemed-coder` | `coder` | Can open a session in editing mode (Apply enabled) | | `kodemed-viewer` | `viewer` | Can only view codes (Apply disabled) | ### 4.4 `readOnly` flag precedence — *Planned (#491-D)* A future `readOnly` flag on the create-session request will take priority over the token roles. The flag is **not yet** present on `CodingSessionRequest`; today the only read-only path is `GET /api/v1/coding/session/last` (replays the user's last completed session in read-only mode). | `readOnly` flag (planned) | User role | Result | |---|---|---| | `false` (or omitted) | `kodemed-coder` | Editing mode (Apply visible) | | `false` (or omitted) | `kodemed-viewer` | Read-only mode (Apply hidden) | | `true` | `kodemed-coder` | Read-only mode (Apply hidden) — flag wins | | `true` | `kodemed-viewer` | Read-only mode (Apply hidden) | ## 5. API Endpoint Specification ### 5.1 Versioning The API is versioned. Current version is `v1`. Non-backwards-compatible changes will produce a new version (`v2`). **Base path:** `/api/v1/coding/` (HIS REST callers send `source=API`; the same endpoint serves DLL and browser clients via different `source` values). ### 5.2 Create Session **Request:** ```http POST /api/v1/coding/session HTTP/1.1 Host: server.kodemed.example Authorization: Bearer Content-Type: application/json { "data": "...XML SPIGES content...", "format": "spiges", "source": "API", "instanceId": "his-case-12345" } ``` **Field reference (see `CodingSessionRequest` in the OpenAPI):** | Field | Type | Required | Description | |---|---|---|---| | `data` | string (XML) | yes | Case data in SPIGES format (max 5 MB) | | `format` | string | yes for `source=API` | Wire format of `data`. Today the only supported value is `"spiges"`. DLL/UI callers may omit. | | `source` | enum | yes for HIS REST | `"API"` for HIS REST integrations. DLL/UI clients send `"DLL"` / `"BROWSER"` (or omit; default is `DLL`). | | `instanceId` | string (≤100 chars) | yes, **unique** | HIS-side correlator for the submitted `data` payload. One value per POST, regardless of how many SPIGES `` cases ride inside `data` — they all belong to the same coding session and share this id. **MUST be unique across active sessions** — a second POST with an `instanceId` still in use returns `409 Conflict` (see §9.1). The id becomes free again once the original session completes / is cancelled / expires. Echoed back in the webhook as `instance_id` so the HIS matches the result against its original submission. | **Webhook URL and HMAC secret are configured per-tenant on the KodeMed side via the IntegrationProfile admin API — they are not part of the create-session request.** **Success response:** ```http HTTP/1.1 201 Created Content-Type: application/json { "sessionId": "550e8400-e29b-41d4-a716-446655440000", "redirectUrl": "https://coding-ui.kodemed.example/spiges/session/550e8400-e29b-41d4-a716-446655440000/cases", "expiresAt": "2026-05-08T22:00:00Z" } ``` **Error responses:** | HTTP | Reason | Body | |---|---|---| | 400 | Body invalid (missing field, malformed XML, `format` missing when `source=API`) | `{ "error": "VALIDATION_ERROR", "details": [...] }` | | 401 | Token missing or expired | `{ "error": "UNAUTHENTICATED" }` | | 403 | Token issuer not in allowlist, or insufficient role | `{ "error": "FORBIDDEN", "reason": "..." }` | | 409 | (HIS REST) `instanceId` already in use by an active coding session | `CodingSessionResponse` with `hasConflict=true`, `conflictSessionId`, `conflictInstanceId`, `conflictMessage` | | 409 | (DLL only) An active DLL session already exists for this user and `forceClose=false` | `CodingSessionResponse` with `hasConflict=true` | | 429 | Rate limit exceeded | `{ "error": "RATE_LIMIT_EXCEEDED", "retryAfter": 60 }` | | 500 | Internal error | `{ "error": "INTERNAL_ERROR", "traceId": "..." }` | ### 5.3 Webhook callback (KodeMed → HIS) **Triggered when:** Coder presses **Apply** or **Discard** on the session UI. **Request:** ```http POST HTTP/1.1 Authorization: HMAC-SHA256 t=1715205912,v1= Content-Type: application/json Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000:case.coded { "event_type": "case.coded", "occurred_at": "2026-05-08T21:45:12Z", "session_id": "550e8400-e29b-41d4-a716-446655440000", "instance_id": "his-case-12345", "spiges": { "ent_id": "01.01.01.01", "burnr": "0000000001", "fall_id": "F-2026-00001" }, "result_data": "...modified XML SPIGES with codes..." } ``` > The `Authorization` header is present **only** when the IntegrationProfile is configured with `webhookSigning=HMAC_SHA256`. The signature is computed as `HMAC-SHA256(secret, ".")` and hex-encoded; the signed string is the unix timestamp `t`, a literal dot, and the raw request body. `Idempotency-Key` is always present (`:`) — use it for deduplication. **Field reference (see `HisWebhookPayload` in the OpenAPI):** | Field | Type | Description | |---|---|---| | `event_type` | enum | `case.coded` (Apply) or `case.discarded` (Discard) | | `occurred_at` | ISO-8601 | When the coder completed the session | | `session_id` | UUID v4 | KodeMed session ID (echo of create response) | | `instance_id` | string | Echo of original request `instanceId` — use this to match the result on the HIS side | | `spiges` | object | SPIGES correlation triplet extracted from the result XML: `{ ent_id, burnr, fall_id }` | | `result_data` | string (XML) | Modified SPIGES (non-empty when `event_type=case.coded`; omitted/empty when `event_type=case.discarded`) | **Expected HIS response:** - `200 OK` → success, KodeMed marks delivery as completed - `4xx` (except 408, 429) → permanent failure, no retry - `5xx`, `408`, `429`, network error → KodeMed retries with exponential backoff (configurable, default 3 retries) **Idempotency:** HIS should treat the webhook as **at-least-once delivery**. The `Idempotency-Key` header carries `:` and is stable across retries — HIS should deduplicate on it. **Signature verification (when HMAC enabled):** The `Authorization: HMAC-SHA256 t=,v1=` header carries the signature. To verify, the HIS recomputes `HMAC-SHA256(shared_secret, ".")` (hex-encoded) and constant-time-compares to `v1`. Reject anything older than a small skew window (e.g. 5 min) to defeat replay. The HMAC secret is exchanged out-of-band when the IntegrationProfile is provisioned. ## 6. UI behaviour ### 6.1 Look & feel The KodeMed UI shown via the create-session `redirectUrl` is the **same normal UI** as KodeMed. No "stripped-down" or "embedded" version — the user sees the full coding experience. ### 6.2 readOnly mode — *Planned (#491-D)* The full readOnly behaviour below (request-flag-driven Apply suppression, READ ONLY badge wired to the HIS contract) is target behaviour under **#491-D**. Today the same UI is rendered for every HIS REST session; the readOnly badge only appears when the session is opened via `GET /api/v1/coding/session/last`. When `readOnly: true` (planned): - **Apply** button hidden - All inputs disabled - Visible **READ ONLY** badge in the header - **Discard** (or "Close") button remains available to close the session The `readOnly` flag is set by the HIS when the SPIGES cases have **already been coded** — the session is for review/audit only, not for editing. ### 6.3 Iframe vs new window HIS can open the create-session `redirectUrl` either in: - `