Skip to content

OSCAR API Protocol Analysis

Last Updated: 2026-03-09

Status: PRODUCTION — REST protocol extension deployed, 98 unit + 8 integration tests


Executive Summary

OSCAR EMR exposes three separate API surfaces with different authentication, content types, and deployment conditions. Our OscarSoapAdapter currently uses Surface 1 (SOAP) for 9 of 11 methods. For Kai-hosted instances behind Cloudflare WAF, SOAP is blocked — only Surface 3 (OAuth REST) works. This analysis documents every API surface, every adapter method, and the proven Cloudflare behavior, informing the preferRest protocol extension.


The Three API Surfaces

OSCAR runs three API namespaces on the same Tomcat instance:

                         OSCAR EMR Server (Tomcat)
┌────────────────────────────────────────────────────────────────────────┐
│                                                                        │
│  SURFACE 1: SOAP Web Services                                         │
│  ─────────────────────────────                                         │
│  Path:    /oscar/ws/{ServiceName}                                      │
│  Proto:   SOAP 1.1 XML                                                 │
│  Auth:    WS-Security UsernameToken (username + password in SOAP hdr)  │
│  C-Type:  text/xml                                                     │
│  Deploy:  ALWAYS present (applicationContext.xml, unconditional)        │
│  Count:   14 services (Provider, Demographic, Schedule, Booking, ...)  │
│                                                                        │
│  SURFACE 2: Cookie-Authenticated REST                                  │
│  ────────────────────────────────────                                   │
│  Path:    /oscar/ws/rs/{resource}                                      │
│  Proto:   REST (JSON)                                                  │
│  Auth:    JSESSIONID cookie — OR — OAuth 1.0a signed request           │
│  C-Type:  application/json                                             │
│  Deploy:  ALWAYS present (applicationContext.xml, unconditional)        │
│  Count:   250+ endpoints (190KB WADL)                                  │
│                                                                        │
│  SURFACE 3: OAuth-Protected REST                                       │
│  ────────────────────────────────                                       │
│  Path:    /oscar/ws/services/{resource}                                │
│  Proto:   REST (JSON)                                                  │
│  Auth:    OAuth 1.0a HMAC-SHA1 (consumer key + access token)           │
│  C-Type:  application/json                                             │
│  Deploy:  CONDITIONAL — only when ModuleNames=REST in oscar.properties │
│  Count:   30+ endpoint beans behind OAuthInterceptor                   │
│  Also:    /oscar/ws/oauth/* (OAuth token lifecycle: initiate,          │
│           authorize, token) — deployed by same module                   │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

Why Three Surfaces Exist

OSCAR was built SOAP-first (Surface 1). The /ws/rs/ REST endpoints (Surface 2) came later for internal OSCAR UI use — they authenticate via browser session cookies. The /ws/services/ OAuth REST endpoints (Surface 3) were added specifically for third-party integrations like Cortico, gated behind ModuleNames=REST so clinics only deploy them when they need external API access.

The ModuleNames=REST Gate

oscar.properties:
  #ModuleNames=REST          ← COMMENTED OUT by default in stock OSCAR

OscarSpringContextLoader.java:
  String moduleNames = OscarProperties.getInstance().getProperty("ModuleNames");
  if (moduleNames != null) {
      for (String module : moduleNames.split(",")) {
          contextLoader.loadContext("applicationContext" + module.trim() + ".xml");
      }
  }

applicationContextREST.xml:  ← ONLY loaded when ModuleNames includes "REST"
  <jaxrs:server id="oauthService"  address="/oauth">     → deploys /ws/oauth/*
  <jaxrs:server id="restServices"  address="/services">   → deploys /ws/services/*
  • Without ModuleNames=REST: CXF listing at /oscar/ws/ shows 14 SOAP + 1 REST (/ws/rs)
  • With ModuleNames=REST: CXF listing shows 14 SOAP + 3 REST (/ws/rs, /ws/services, /ws/oauth)

Server Inventory

Property Dev OSCAR Kai OSCAR Pro (FreshBay)
Host 15.222.50.48:8443 fbh.kai-oscar.com
Build OpenOSP (open source) Kai OSCAR Pro (WELL Health)
Infrastructure AWS EC2 (t3a.medium) WELL Health cloud
Cloudflare No Yes (WAF active, YYZ PoP)
ModuleNames=REST SET (all 3 REST surfaces) NOT SET (only /ws/rs)
SOAP All 14 services work CF blocks SOAP envelope
Use case Development + testing First production customer

Cloudflare WAF Behavior

CF blocks SOAP requests to Kai-hosted OSCAR

The Cloudflare WAF on Kai-hosted instances uses content-inspection to block SOAP requests. Any request with Content-Type: text/xml AND a body containing <soapenv:Envelope is blocked with HTTP 403. REST requests with Content-Type: application/json are never blocked.

CF Decision Logic

    Incoming Request to fbh.kai-oscar.com
   ┌──────────────────────────┐
   │  Cloudflare WAF          │
   │                          │
   │  Content-Type = text/xml │──NO──► PASS to OSCAR
   │  ?                       │
   └───────────┬──────────────┘
               │ YES
   ┌──────────────────────────┐
   │  Body contains           │
   │  <soapenv:Envelope ?     │──NO──► PASS to OSCAR
   │                          │
   └───────────┬──────────────┘
               │ YES
        ┌──────────────┐
        │  HTTP 403     │
        │  CF blocks    │
        │  Kai HTML err │
        └──────────────┘

The 8-Test Proof Matrix

All tests run from 132.145.100.100 (our OCI server) on 2026-02-26:

# URL Path Content-Type Body HTTP Origin
A /ws/ProviderService text/xml SOAP envelope 403 CF BLOCK
F /ws/DemographicService text/xml SOAP envelope 403 CF BLOCK
G /ws/ProviderService text/xml plain XML (no SOAP) 500 OSCAR
E /ws/ProviderService text/xml (empty body) 500 OSCAR
B /ws/ProviderService application/json JSON 500 OSCAR
H /ws/ProviderService application/json JSON w/ "soap" string 500 OSCAR
C /ws/services/schedule/add application/json JSON 404 OSCAR
D /ws/services/schedule/add text/xml XML fragment 404 OSCAR

Key conclusions:

  • Tests A+F: SOAP envelope on SOAP path — CF blocks (403)
  • Tests G+E: Same SOAP path but no SOAP envelope — CF passes (500 = OSCAR error)
  • Tests B+H: Same SOAP path but JSON — CF passes (500 = OSCAR error)
  • Tests C+D: REST path with JSON or XML — CF passes (404 = REST module not loaded)
  • Our REST calls use Content-Type: application/json — will NEVER be blocked by CF.

quickSearch Parameter

Correct parameter name

OSCAR CXF uses @QueryParam("query") annotation. The correct URL is ?query=SEARCHTERM. Using ?searchString or ?term returns {query: null, total: 0} — not an error, just empty results. Validated on both dev OSCAR and Kai (2026-03-01).

warmUp and Cold TLS

For preferRest adapters connecting to Kai OSCAR (Cloudflare-fronted), the first TLS handshake takes ~6 seconds (CF certificate exchange + connection pooling). The circuit breaker timeout is 4 seconds. Without pre-warming the adapter, the first production call after cache miss will trip the breaker.

Fix: EmrAdapterFactory now awaits warmUp() for preferRest adapters. PM2 startup also warms all known preferRest clinics via IIFE in index.ts.


Current Adapter: Per-Method Analysis

OscarSoapAdapter.ts — 1,298 lines, 11 public methods. Here is every method's protocol, URL, auth, and per-server behavior:

Request Details

# Method Protocol URL HTTP C-Type Auth
1 getProviders() SOAP /ws/ProviderService POST text/xml WS-Security
2 getProvider(id) SOAP (delegates to #1) POST text/xml WS-Security
3 getScheduleSlots() SOAP /ws/ScheduleService POST text/xml WS-Security
4 getScheduleTemplateCodes() SOAP /ws/ScheduleService POST text/xml WS-Security
5 getAppointments() SOAP /ws/ScheduleService POST text/xml WS-Security
6 createAppointment() SOAP /ws/ScheduleService POST text/xml WS-Security
7 cancelAppointment() SOAP /ws/ScheduleService POST text/xml WS-Security
8 getPatient(id) SOAP /ws/DemographicService POST text/xml WS-Security
9 searchPatient(name) SOAP /ws/DemographicService POST text/xml WS-Security
10 searchPatient(phone) REST /ws/rs/demographic/search GET app/json OAuth 1.0a
11 createPatient() REST /ws/services/demographics POST app/json OAuth 1.0a

Per-Server Status (Current)

# Method Dev OSCAR Kai FreshBay
1 getProviders() :white_check_mark: Works :x: CF blocks SOAP envelope
2 getProvider(id) :white_check_mark: Works :x: CF blocks
3 getScheduleSlots() :white_check_mark: Works :x: CF blocks SOAP envelope
4 getScheduleTemplateCodes() :white_check_mark: Works :x: CF blocks SOAP envelope
5 getAppointments() :white_check_mark: Works :x: CF blocks SOAP envelope
6 createAppointment() :white_check_mark: Works :x: CF blocks SOAP envelope
7 cancelAppointment() :white_check_mark: Works :x: CF blocks SOAP envelope
8 getPatient(id) :white_check_mark: Works :x: CF blocks SOAP envelope
9 searchPatient(name) :white_check_mark: Works :x: CF blocks SOAP envelope
10 searchPatient(phone) :white_check_mark: Works :warning: CF passes, auth unclear
11 createPatient() :white_check_mark: Works :x: 404 (REST module not deployed)

Scorecard

Dev OSCAR: 11/11 methods work. Kai FreshBay: 0/11 methods work for production use.


The Fix: preferRest Flag

What Changes

Add preferRest: boolean to OscarSoapAdapter. When true, all 11 methods route to /ws/services/* (Surface 3) using OAuth 1.0a signed JSON requests instead of SOAP XML.

Implemented Routing (Production)

# Method preferRest=false (DEFAULT) preferRest=true (Kai)
1 getProviders() SOAP /ws/ProviderService GET /ws/services/providerService/providers (3-tier fallback: JSON→XML→DB)
2 getProvider(id) delegates to #1 + filter delegates to #1 + filter
3 getScheduleSlots() SOAP /ws/ScheduleService GET /ws/services/schedule/{id}/day/{date}
4 getScheduleTemplateCodes() SOAP /ws/ScheduleService GET /ws/services/schedule/codes (fallback: /scheduleTemplate)
5 getAppointments() SOAP /ws/ScheduleService GET /ws/services/schedule/{id}/day/{date} (iterated per day)
6 createAppointment() SOAP /ws/ScheduleService POST /ws/services/schedule/add (NewAppointmentTo1 format)
7 cancelAppointment() SOAP /ws/ScheduleService POST /ws/services/schedule/appointment/{id}/updateStatus (status='C')
8 getPatient(id) SOAP /ws/DemographicService GET /ws/services/demographics/{id}
9 searchPatient(name) SOAP /ws/DemographicService GET /ws/services/demographics/quickSearch?query={term}
10 searchPatient(phone) REST /ws/rs/demographic/search GET /ws/services/demographics/quickSearch?query={phone} (fallback: /ws/rs/)
11 createPatient() REST /ws/services/demographics POST /ws/services/demographics (unchanged)

REST API Gotchas (Validated in Production)

  • quickSearch: Parameter is ?query= (CXF @QueryParam("query")), NOT ?searchString= or ?term=. Wrong param silently returns {query: null, total: 0}.
  • schedule/add: Expects NewAppointmentTo1 body, NOT AppointmentTo1. Key: startTime is "HH:mm" (no seconds), duration is required (endTime calculated server-side), status must not be null (causes NPE).
  • cancelAppointment: Endpoint is /schedule/appointment/{id}/updateStatus, NOT /schedule/updateAppointment.
  • Provider JSON 406: JAXB bug returns 406 for JSON. Fallback: request XML with Accept: application/xml, then parse XML. Further fallback: direct DB query via ClinicProvider table.

Per-Server Status AFTER preferRest

# Method Dev OSCAR Kai (after ModuleNames=REST)
1-11 All methods :white_check_mark: 11/11 :white_check_mark: 11/11

Prerequisite

Kai must enable ModuleNames=REST in oscar.properties and restart Tomcat. Without that, Kai scores 0/11 regardless of our code changes.


Request Lifecycle: Before vs After

BEFORE: getProviders() on Kai (FAILS)

  OscarSoapAdapter.getProviders()
  node-soap builds SOAP XML envelope:
  ┌─────────────────────────────────────────────────────────┐
  │ POST /oscar/ws/ProviderService HTTP/1.1                 │
  │ Host: fbh.kai-oscar.com                                 │
  │ Content-Type: text/xml; charset=utf-8                   │
  │                                                         │
  │ <soapenv:Envelope xmlns:soapenv="...">                  │
  │   <soapenv:Header>                                      │
  │     <wsse:Security mustUnderstand="1">                  │
  │       <wsse:UsernameToken>                              │
  │         <wsse:Username>vitara</wsse:Username>           │
  │         <wsse:Password>FreshBay999</wsse:Password>      │
  │       </wsse:UsernameToken>                             │
  │     </wsse:Security>                                    │
  │   </soapenv:Header>                                     │
  │   <soapenv:Body>                                        │
  │     <ws:getProviders2><arg0>true</arg0></ws:getProviders│
  │   </soapenv:Body>                                       │
  │ </soapenv:Envelope>                                     │
  └──────────────────────┬──────────────────────────────────┘
              Cloudflare WAF (YYZ PoP)
              ┌────────────────────────┐
              │ text/xml? YES          │
              │ SOAP envelope? YES     │
              │ → BLOCK                │
              └──────────┬─────────────┘
              HTTP 403 + Kai HTML error page
              (never reaches OSCAR)

AFTER: getProviders() on Kai with preferRest=true (WORKS)

  OscarSoapAdapter.getProviders()
         │ preferRest=true → oauthRestCall()
  OAuth 1.0a signs the request (HMAC-SHA1):
  ┌──────────────────────────────────────────────────────────┐
  │ GET /oscar/ws/services/providerService/providers HTTP/1.1│
  │ Host: fbh.kai-oscar.com                                  │
  │ Content-Type: application/json                           │
  │ Authorization: OAuth oauth_consumer_key="3stz8h7nah...", │
  │   oauth_token="<access_token>",                          │
  │   oauth_signature_method="HMAC-SHA1",                    │
  │   oauth_signature="<computed>", ...                      │
  └──────────────────────┬───────────────────────────────────┘
              Cloudflare WAF (YYZ PoP)
              ┌────────────────────────┐
              │ text/xml? NO (app/json)│
              │ → PASS                 │
              └──────────┬─────────────┘
              OSCAR Tomcat
              ┌────────────────────────┐
              │ /ws/services/ matched  │
              │ OAuthInterceptor       │
              │ validates signature    │
              │ → returns JSON         │
              └──────────┬─────────────┘
              JSON response → transformProvider() → AdapterResult

Authentication Comparison

Aspect SOAP (WS-Security) REST (OAuth 1.0a)
Where credentials go Inside SOAP XML header HTTP Authorization header
Credential type Username + plaintext password Consumer key+secret + token+secret
Signature None (PasswordText) HMAC-SHA1 over method+url+params
Replay protection None (no timestamp/nonce) Timestamp + nonce in every request
Setup needed _ws_ role on OSCAR user OAuth client + 3-legged auth flow
Token lifecycle Password never expires Access token (TTL=365000)
CF WAF interaction text/xml triggers block app/json never triggers block
Security posture Plaintext password in XML over TLS Signed requests, no shared secret in transit

Every Auth Method Tested

We tested 10 authentication approaches against Kai FreshBay. Only one path forward:

# Auth Method Verdict Why
1 OAuth 1.0a 3-legged REST :white_check_mark: VIABLE The Cortico model. Needs ModuleNames=REST.
2 SOAP WS-Security :x: CF blocks SOAP envelope + 500 on WS-Security
3 OAuth 2.0 / SMART FHIR :x: 404 — not deployed (browser-verified)
4 2-Legged OAuth :x: OSCAR code rejects (preAuthorizedToken always null)
5 Basic Auth :x: 401 — not supported
6 Bearer Token :x: 401 — ignored by OSCAR
7 API Key Header :x: 401 — not recognized
8 Session Cookie :x: CF blocks /login.do, LoginService returns 500
9 Negotiate/NTLM :x: 401 — not supported
10 Direct DB Token Insert :x: Needs MySQL access (unavailable) + still needs REST module

Auth Method Elimination Funnel

              AUTH METHOD ELIMINATION (Kai OSCAR Pro)
┌──────────────────────────────────────────────────────────────┐
│  10 Authentication Methods Tested Against Kai FreshBay       │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  SOAP WS-Security ──────────► BLOCKED by CF WAF             │
│  (Content-Type: text/xml)     (content-inspection rule)      │
│                                                              │
│  Session Cookie ────────────► CF blocks /login.do            │
│  (JSESSIONID)                 LoginService returns 500       │
│                                                              │
│  OAuth 2.0 / SMART FHIR ───► 404 — not deployed on Kai     │
│                                                              │
│  2-Legged OAuth ────────────► Rejected (preAuthorizedToken   │
│                                always null in OSCAR code)    │
│                                                              │
│  Basic Auth ────────────────► 401 — not supported            │
│  Bearer Token ──────────────► 401 — ignored by OSCAR         │
│  API Key Header ────────────► 401 — not recognized           │
│  Negotiate/NTLM ───────────► 401 — not supported            │
│  Direct DB Token Insert ────► No MySQL access available      │
│                                                              │
│  ════════════════════════════════════════════════════════     │
│  OAuth 1.0a 3-Legged REST ──► WORKS                         │
│  (HMAC-SHA1 signed JSON)      JSON passes CF WAF             │
│  (/ws/services/*)             The Cortico model              │
│  ════════════════════════════════════════════════════════     │
│                                                              │
│  Result: OAuth 1.0a REST is the ONLY viable auth path        │
│  for Kai-hosted OSCAR behind Cloudflare.                     │
│  Prerequisite: ModuleNames=REST in oscar.properties          │
└──────────────────────────────────────────────────────────────┘

The Cortico Parallel

Cortico is the largest third-party integration for OSCAR (~560 clinics on Kai). Their architecture validates our approach:

  Cortico's Integration Model            Our Model (preferRest=true)
  ─────────────────────────               ──────────────────────────

  1. Ask clinic to enable                 1. Ask clinic to enable
     ModuleNames=REST                        ModuleNames=REST
           ↓                                       ↓
  2. 3-legged OAuth flow                  2. 3-legged OAuth flow
     (browser authorize step)                (browser authorize step)
           ↓                                       ↓
  3. All API calls via                    3. All API calls via
     OAuth REST /ws/services/*               OAuth REST /ws/services/*
           ↓                                       ↓
  4. CF passes JSON traffic               4. CF passes JSON traffic
     — no IP allowlist needed                — no IP allowlist needed

Cortico never mentions Cloudflare, IP allowlisting, or SOAP because they don't use any of those. Neither will we.


Decision Flow Per Method Call

  ┌────────────────────────┐
  │  adapter.someMethod()  │
  └───────────┬────────────┘
  ┌──────────────────────────────────┐
  │  this.preferRest                  │
  │  && this.oauthClient != null ?    │
  └──────┬───────────────┬───────────┘
         │ YES           │ NO
         ▼               ▼
  ┌──────────────┐  ┌─────────────────┐
  │ oauthRestCall │  │ SOAP path       │
  │ (helper)      │  │ (existing code  │
  │               │  │  100% unchanged)│
  └──────┬───────┘  └────────┬────────┘
         │                    │
    ┌────┴────┐          return result
  SUCCESS   FAIL
    │         │
    ▼         ▼
  return   return REST error
  data     (NO SOAP fallback)

No REST-to-SOAP Fallback

If CF blocks SOAP, falling back to SOAP after a REST failure just adds latency before the same 403. The preferRest flag is a clean switch — REST or SOAP, not both.


Circuit Breaker Architecture

  ┌─────────────────────────────────────────────────────────────────┐
  │                    Circuit Breakers                              │
  │                                                                 │
  │  SOAP breakers (existing, unchanged):                          │
  │    scheduleBreaker   ──► SOAP ScheduleService calls            │
  │    demographicBreaker──► SOAP DemographicService calls         │
  │    providerBreaker   ──► SOAP ProviderService calls            │
  │                                                                 │
  │  REST breaker (NEW, separate):                                 │
  │    restBreaker       ──► ALL OAuth REST calls via oauthRestCall│
  │                                                                 │
  │  Config: 4s timeout, 50% error threshold, 30s reset            │
  └─────────────────────────────────────────────────────────────────┘

The REST breaker is separate from SOAP breakers to prevent cross-contamination: if REST fails and trips a SOAP breaker, both paths die simultaneously. Separate breakers isolate failure domains.


Implementation Impact

What Changes

File Changes
OscarSoapAdapter.ts +preferRest field, +restBreaker, +oauthRestCall() helper (~40 lines), +REST branch in 8 methods (~15-25 each), updated warmUp() and healthCheck()
EmrAdapterFactory.ts Read oscarPreferRest from DB, pass to adapter
schema.prisma +oscarPreferRest Boolean @default(false)

What Doesn't Change

File Reason
IEmrAdapter.ts Interface unchanged
OscarBridgeAdapter.ts Unrelated adapter
oscar-oauth.ts OAuth flow unchanged
All SOAP code paths Zero changes when preferRest=false
All existing tests Zero changes

Total: ~585 lines added, ~17 modified, 0 deleted.


Risk Register

Severity Risk Status Mitigation
P0 REST POST blocked by CF :white_check_mark: PASSED 8-test matrix proves content-inspection
P0 REST response format unknown Pending Capture fixtures from dev OSCAR before coding
P0 ModuleNames=REST is third-party dep Pending Send email to Kai (identical to Cortico onboarding)
P1 /ws/services/ vs /ws/rs/ endpoint differences Capture WADL after Kai enables REST
P1 OAuth token silent expiration Daily health check + alert
P1 Dev vs Kai response format divergence Capture + diff fixtures from both servers
P2 preferRest boolean wrong abstraction Pragmatic for 1 customer; refactor if third protocol needed

OSCAR Build Comparison

Property Dev (OpenOSP) Kai Pro (FreshBay)
DemographicService methods 9 14
ScheduleService methods ~14 18
RESTful CXF services 3 (/rs, /services, /oauth) 1 (/rs only)
SOAP services 14 14
ModuleNames=REST SET NOT SET
Server timezone UTC PST (UTC-8)
Cloudflare None Yes (YYZ PoP)
Custom error pages No (std Tomcat) Yes (Kai branded)
WS-Security result Works 500 HTML error

Why Not Alternatives?

Alternative Viable? Why Not
CF IP allowlist + keep SOAP Maybe Kai controls CF, may refuse. Even if granted, WS-Security returns 500.
New OscarRestAdapter class Yes Rejected by CTO — duplicates ~400 lines of shared transformers, circuit breakers, template cache.
Use /ws/rs/ (Surface 2) with cookies Risky Cookie-auth is undocumented for server-to-server. No WADL format equivalence.
Session cookie via web login No CF blocks /login.do. LoginService returns 500.
Direct database access No Kai-hosted = no DB access. PHIPA/PIPA prohibit custom components on customer EMRs.
Wait for FHIR/OAuth 2.0 No 404 on /.well-known/smart-configuration. Not deployed, no timeline.

Remaining Stop-Gates

# Gate Status Action
1 REST POST passes Cloudflare :white_check_mark: PASSED 8-test matrix, 2026-02-26
2 Capture REST response fixtures from dev OSCAR :hourglass: PENDING Write capture-rest-fixtures.ts, run against dev
3 ModuleNames=REST email sent to Kai :hourglass: PENDING CK to confirm via clinic admin


Analysis based on 26+ curl tests, 9 research agents, 4 browser verifications, and OSCAR source code analysis. Date: 2026-02-26.