medOS Public API — Integration Guide
Service:
ever-public-api· Port: 8082 · Base path:/fhirThe 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.
- Customer requests an integration. Super-admin navigates to
/super-admin/fhir-integrationsin the medOS web app. - 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). - Operations —
read,write,patch,delete,webhook(multi-select). - Delivery — optional webhook endpoint + rate limit
(
rate_limit_per_min, default 100).
- 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.) - 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
env | Purpose | Data |
|---|---|---|
lk | Production / live | Real PHI |
tk | Tenant test / staging | Realistic non-PHI |
sk | Sandbox | Synthetic 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:
| Field | Effect |
|---|---|
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):
status≠active→ denyexpires_at< now → denyoperation∉allowed_operations→ denyclinical_domains == []ANDresource_types == []→ allow (wildcard)resourceType ∈ resource_types→ allow- resource's domain ∈
clinical_domains(withresource_types == []) → allow - resource's domain ∈
clinical_domainsANDresourceType ∈ resource_types→ allow - 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:
| Status | issue.code | Meaning |
|---|---|---|
400 | invalid | Malformed request body or search param |
401 | security | Missing / unknown / expired bearer |
403 | forbidden / security | Scope deny, integration revoked/suspended |
404 | not-found | Resource id doesn't exist |
409 | conflict | Version conflict (e.g. If-Match mismatch) |
422 | business-rule | Validation failure (e.g. HL7v2 parse error) |
429 | throttled | Rate limit exceeded |
501 | not-supported | Operation not configured (e.g. SMART without OAuth) |
503 | transient | Downstream 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.5on read requests where available. - Read pathology/clinical-domain
displayvalues from the firstcoding[]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.
13. Related catalog items
INT-1— FHIR R4 Read APIINT-2— FHIR R4 Write APIINT-3— FHIR Subscriptions and WebhooksINT-4— HL7v2 ingest (REST + MLLP)INT-5— FHIR Bulk Data ($export)SEC-8— SMART-on-FHIR Scope GuardSEC-9— FHIR Integration Key GuardDEV-2— Public REST API