Skip to main content

medOS Public API — Integration Guide

Service: ever-public-api · Port: 8082 · Base path: /fhir

The medOS Public API is the FHIR R4 and HL7v2 entry point for external systems (legacy EMRs, lab/imaging vendors, payer integrators, BI tools). This page is the canonical onboarding guide; everything an integrator needs to consume our APIs lives here.

1. Onboarding flow

Every external system must hold a medOS scoped API key. Keys are minted by a super-admin and bind a third-party caller to a finite set of clinical domains, FHIR resource types, and operations.

  1. Customer requests an integration. Super-admin navigates to /super-admin/fhir-integrations in the medOS web app.
  2. Fills the 4-step wizard:
    • Identity — integration name, owner email, optional description.
    • Scopes — pick clinical domains (e.g. clinical_records, medications, lab_results) and/or specific FHIR resource types (e.g. Patient, Encounter, Observation).
    • Operationsread, write, patch, delete, webhook (multi-select).
    • Delivery — optional webhook endpoint + rate limit (rate_limit_per_min, default 100).
  3. On submit, the system mints a bearer of the form fhir_<env>_<prefix>_<secret> and shows it once in the reveal modal. The integrator must copy it immediately — medOS never displays the secret again. (A super-admin can rotate to issue a fresh secret if it is lost.)
  4. Integrator stores the bearer in their own secret manager.

Bearer format

fhir_lk_a1b2c3d4_ZXhhbXBsZS1zZWNyZXQtY2hhcnMtMzItbG9uZw
└─┬─┘└┬┘└───┬───┘└───────────────────┬───────────────────┘
│ │ │ │
│ │ │ └─ 32-char base64url secret
│ │ └─ 8-char public prefix (safe to log)
│ └─ environment: lk=live | tk=test | sk=sandbox
└─ fixed prefix

Only the prefix is safe to log. The full bearer is never persisted in plain text on our side — Supabase stores a bcrypt hash and looks it up on each request.

Environments

envPurposeData
lkProduction / liveReal PHI
tkTenant test / stagingRealistic non-PHI
skSandboxSynthetic data only

Use a sk key when wiring up an integration for the first time.

2. Calling the API

Required headers

Authorization: Bearer fhir_lk_a1b2c3d4_ZXhhbXBsZS1zZWNyZXQtY2hhcnMtMzItbG9uZw
Accept: application/fhir+json

POST/PUT/PATCH bodies are application/fhir+json or application/json-patch+json (PATCH).

Capability statement

curl https://api.medos.health/fhir/metadata

Returns the FHIR R4 CapabilityStatement listing the 97 resource types and their supported interactions. Use this to discover what your scope can access.

Curl recipes

Read a patient

curl https://api.medos.health/fhir/Patient/65a1b2c3d4e5f60012345678 \
-H "Authorization: Bearer $MEDOS_BEARER" \
-H "Accept: application/fhir+json"

Search patients

curl 'https://api.medos.health/fhir/Patient?family=Doe&birthdate=1990-01-01' \
-H "Authorization: Bearer $MEDOS_BEARER"

Search returns a Bundle of type=searchset with entry[].resource.

List a patient's encounters

curl 'https://api.medos.health/fhir/Encounter?patient=Patient/65a1b2c3d4e5f60012345678&_sort=-date&_count=50' \
-H "Authorization: Bearer $MEDOS_BEARER"

Pull observations (labs / vitals)

curl 'https://api.medos.health/fhir/Observation?patient=Patient/65a...&category=laboratory&date=ge2026-01-01' \
-H "Authorization: Bearer $MEDOS_BEARER"

Pull diagnostic reports

curl 'https://api.medos.health/fhir/DiagnosticReport?patient=Patient/65a...&_include=DiagnosticReport:result' \
-H "Authorization: Bearer $MEDOS_BEARER"

Create a new condition (write)

curl -X POST https://api.medos.health/fhir/Condition \
-H "Authorization: Bearer $MEDOS_BEARER" \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType": "Condition",
"subject": { "reference": "Patient/65a..." },
"code": { "coding": [{ "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "I10" }] },
"clinicalStatus": { "coding": [{ "code": "active" }] }
}'

Returns the created resource with server-assigned id and meta.versionId. Write operations require write in your integration's allowed_operations, and the deployment-wide fhir.write.enabled tenant flag must be on.

3. Scope model

Scope is the intersection of three sets:

FieldEffect
clinical_domains (e.g. ["clinical_records", "medications"])Wide grant — every resource type tagged with that domain.
resource_types (e.g. ["Patient", "Encounter"])Narrow grant — exact resource list.
allowed_operations (e.g. ["read"])Verb gate — applied on top of resource grant.

Decision algorithm (matches services/public-api/.../guards/fhir-integration-scope.service.ts):

  1. statusactivedeny
  2. expires_at < now → deny
  3. operationallowed_operationsdeny
  4. clinical_domains == [] AND resource_types == []allow (wildcard)
  5. resourceType ∈ resource_typesallow
  6. resource's domain ∈ clinical_domains (with resource_types == []) → allow
  7. resource's domain ∈ clinical_domains AND resourceType ∈ resource_typesallow
  8. otherwise → deny (out_of_scope)

Every decision is recorded in fhir_integration_audit_log with the actor, resource, operation, IP, user-agent, and reason. Super-admins can review the trail from the integration detail dialog.

4. Rate limiting

Each integration has a rate_limit_per_min value (default 100). The guard increments a per-minute counter in Supabase; when the counter exceeds the limit, the response is:

HTTP/1.1 429 Too Many Requests
Content-Type: application/fhir+json

{ "resourceType": "OperationOutcome", "issue": [ ... ] }

The counter resets every minute. The 1-hour-old rows are pruned by the prune_fhir_rate_counters cron job (registered via /super-admin/cron-jobs).

5. Errors

All errors are FHIR R4 OperationOutcome resources with one of:

Statusissue.codeMeaning
400invalidMalformed request body or search param
401securityMissing / unknown / expired bearer
403forbidden / securityScope deny, integration revoked/suspended
404not-foundResource id doesn't exist
409conflictVersion conflict (e.g. If-Match mismatch)
422business-ruleValidation failure (e.g. HL7v2 parse error)
429throttledRate limit exceeded
501not-supportedOperation not configured (e.g. SMART without OAuth)
503transientDownstream dependency down

6. Webhooks (outbound)

If your integration registers a webhook endpoint, medOS will POST FHIR Bundles (or native domain JSON, depending on the subscription's payloadFormat) when clinical events match the subscription criteria.

Request shape

POST <your-endpoint> HTTP/1.1
Content-Type: application/fhir+json
X-Vajira-Event: clinical.encounter.created
X-Vajira-Signature: sha256=<hex-digest>
X-Vajira-Delivery-ID: <uuid>
X-Vajira-Payload-Format: fhir-r4

{ "resourceType": "Bundle", "type": "history", "entry": [...] }

Verifying the signature

const crypto = require('crypto');
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody) // raw bytes, not parsed JSON
.digest('hex');
if (!timingSafeEqual(expected, req.headers['x-vajira-signature'])) reject();

The webhook secret is shown at integration creation alongside the bearer.

Delivery guarantees

  • 10s timeout per attempt.
  • Retry with exponential backoff up to 10 attempts.
  • Subscription auto-disabled after 10 consecutive failures.
  • Every attempt logged to WebhookDeliveryLog (visible to super-admins).
  • Scope is re-checked at dispatch time. A revoked integration stops receiving webhooks immediately.

7. HL7v2 ingest

Legacy systems can post HL7v2 pipe-delimited messages and receive a FHIR Bundle back.

curl -X POST https://api.medos.health/fhir/hl7v2 \
-H "Authorization: Bearer $MEDOS_BEARER" \
-H "Content-Type: text/plain" \
--data-binary $'MSH|^~\\&|SendApp|SendFac|RecvApp|RecvFac|20260101120000||ADT^A01|MSG001|P|2.5\rPID|1||PAT123^^^MRN||Smith^John^A||19800515|M'

Supported message types: ADT^A01/A02/A03/A08, ORU^R01, ORM^O01. Requires write operation on the Bundle resource (or the interoperability domain).

MLLP (TCP)

For high-volume feeds, point your sending system at the medOS MLLP listener (port 2575 by default — set via HL7V2_MLLP_PORT). Source IPs must be on the deployment's HL7V2_MLLP_ALLOWED_IPS allowlist; unlisted connections are dropped immediately. Operate over a private network or VPN.

8. Bulk Data ($export)

For full cohort syncs, use the FHIR Bulk Data Access IG operations. Three kick-off variants:

# Patient-level
curl https://api.medos.health/fhir/Patient/$id/\$export \
-H "Authorization: Bearer $MEDOS_BEARER"

# Group-level
curl https://api.medos.health/fhir/Group/$id/\$export \
-H "Authorization: Bearer $MEDOS_BEARER"

# System-level (everything in scope)
curl https://api.medos.health/fhir/\$export \
-H "Authorization: Bearer $MEDOS_BEARER"

Each returns 202 Accepted with a Content-Location header pointing at a status URL. Poll the status URL until 200 OK:

curl https://api.medos.health/fhir/\$export-status/<exportId> \
-H "Authorization: Bearer $MEDOS_BEARER"

When ready, the response contains a manifest:

{
"transactionTime": "2026-05-15T08:00:00Z",
"requiresAccessToken": true,
"output": [
{ "type": "Patient", "count": 1, "url": "/fhir/$export-download/<id>/Patient" },
{ "type": "Encounter", "count": 12, "url": "/fhir/$export-download/<id>/Encounter" }
]
}

GET each url to receive application/fhir+ndjson (one resource per line).

9. SMART-on-FHIR

The CapabilityStatement advertises SMART-on-FHIR as a secondary auth scheme, but the SMART launch flow is only active when the deployment has an external OAuth2 provider (Keycloak) configured. If KEYCLOAK_URL is not set, GET /fhir/.well-known/smart-configuration returns 501 Not Implemented with an OperationOutcome directing integrators to the bearer-key flow. Most deployments use scoped bearer keys exclusively.

10. Recipe — e-PHIS legacy EMR pull

A legacy HIS such as e-PHIS or Oracle CDS wants to fetch our patient data to display in its own viewer. Typical pull pattern, once a bearer is minted with scopes clinical_records + medications + lab_results (read only):

# 1. Resolve our patient id from the partner's identifier system.
curl 'https://api.medos.health/fhir/Patient?identifier=https://e-phis.gov/mrn|HN-998877' \
-H "Authorization: Bearer $EPHIS_BEARER"
# → Bundle with entry[0].resource.id = "65a..."

PATIENT_ID=65a1b2c3d4e5f60012345678

# 2. Encounter timeline.
curl "https://api.medos.health/fhir/Encounter?patient=Patient/$PATIENT_ID&_sort=-date" \
-H "Authorization: Bearer $EPHIS_BEARER"

# 3. Current problems + allergies.
curl "https://api.medos.health/fhir/Condition?patient=Patient/$PATIENT_ID&clinical-status=active" \
-H "Authorization: Bearer $EPHIS_BEARER"
curl "https://api.medos.health/fhir/AllergyIntolerance?patient=Patient/$PATIENT_ID" \
-H "Authorization: Bearer $EPHIS_BEARER"

# 4. Active prescriptions.
curl "https://api.medos.health/fhir/MedicationRequest?patient=Patient/$PATIENT_ID&status=active" \
-H "Authorization: Bearer $EPHIS_BEARER"

# 5. Last 30 days of lab observations.
curl "https://api.medos.health/fhir/Observation?patient=Patient/$PATIENT_ID&category=laboratory&date=ge$(date -d '-30 days' -I)" \
-H "Authorization: Bearer $EPHIS_BEARER"

# 6. Diagnostic reports with included results.
curl "https://api.medos.health/fhir/DiagnosticReport?patient=Patient/$PATIENT_ID&_include=DiagnosticReport:result" \
-H "Authorization: Bearer $EPHIS_BEARER"

For an initial full-history backfill, prefer the patient-level $export operation (section 8) over chaining hundreds of search calls.

To receive near-realtime updates instead of polling, register a webhook endpoint when minting the integration. medOS will POST FHIR Bundles on clinical events for resources in your scope.

11. Bilingual / Thai-locale notes

medOS holds bilingual labels (TH + EN) on master-data tables. FHIR resources expose Thai display names via the _language=th parameter where the underlying entity has them, or as a coding[].display value. e-PHIS clients operating in Thai should:

  • Set Accept-Language: th-TH, th;q=0.9, en;q=0.5 on read requests where available.
  • Read pathology/clinical-domain display values from the first coding[] entry; the FHIR mapper places the locale-preferred value first.

12. Postman collection

A starter Postman collection lives at infrastructure/postman/medos-fhir.postman_collection.json. Import it and set the bearer and base_url collection variables to start exploring.

  • INT-1 — FHIR R4 Read API
  • INT-2 — FHIR R4 Write API
  • INT-3 — FHIR Subscriptions and Webhooks
  • INT-4 — HL7v2 ingest (REST + MLLP)
  • INT-5 — FHIR Bulk Data ($export)
  • SEC-8 — SMART-on-FHIR Scope Guard
  • SEC-9 — FHIR Integration Key Guard
  • DEV-2 — Public REST API