Back to blog
healthcarebeginner

Building with Oystehr — Headless EHR Platform Guide

Learn how to build healthcare applications on the Oystehr (formerly Ottehr) headless EHR platform: FHIR resources, patient records, appointments, telehealth, and the Oystehr SDK.

Asma HafeezApril 17, 20265 min read
healthcareehrfhiroystehrtelehealth
Share:𝕏

Building with Oystehr — Headless EHR

Oystehr is a headless Electronic Health Record (EHR) platform built on open standards: FHIR R4, HL7, and SMART on FHIR. Instead of inheriting the UI of legacy EHR systems, you build your own frontend while Oystehr handles the clinical data layer, compliance infrastructure, and integrations.


What Oystehr Provides

FHIR R4 API          — store and query patients, appointments, encounters,
                       medications, observations, and all other FHIR resources
Telehealth           — embedded video consultations (Zoom integration)
Messaging            — secure patient-provider messaging
Scheduling           — appointment booking, availability management
Lab integrations     — order and receive lab results
Prescriptions        — e-prescribing (eRx) via Surescripts
Billing              — claims and insurance eligibility
Compliance           — HIPAA BAA, SOC 2, audit logs built in

Core Concepts

FHIR Resources

Everything in healthcare maps to a FHIR resource:

Patient          — demographic and identifying information
Practitioner     — providers (doctors, nurses, therapists)
Appointment      — scheduled encounters
Encounter        — actual clinical visits
Observation      — vitals, lab results, assessments
Condition        — diagnoses
MedicationRequest — prescriptions
DocumentReference — clinical notes, attachments
Coverage         — insurance information

Oystehr Project Structure

Oystehr Project
├── Applications (your frontend apps)
├── Roles & Permissions (staff, patient, admin)
├── Locations (clinics, virtual)
├── Practitioners
└── FHIR Store (all clinical data)

Authentication

Oystehr uses M2M (machine-to-machine) tokens for server-side code and SMART on FHIR for patient-facing apps.

TYPESCRIPT
// Server-side: M2M token
const { token } = await fetch('https://auth.oystehr.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type:    'client_credentials',
    client_id:     process.env.OYSTEHR_CLIENT_ID,
    client_secret: process.env.OYSTEHR_CLIENT_SECRET,
    audience:      'https://api.oystehr.com'
  })
}).then(r => r.json());

Working with Patients

TYPESCRIPT
import Oystehr from '@oystehr/sdk';

const oystehr = new Oystehr({ accessToken: token });

// Create a patient
const patient = await oystehr.fhir.create({
  resourceType: 'Patient',
  name: [{ family: 'Hansen', given: ['Erik'] }],
  gender: 'male',
  birthDate: '1985-03-15',
  telecom: [
    { system: 'email',   value: 'erik.hansen@example.com' },
    { system: 'phone',   value: '+47 900 00 000',          use: 'mobile' }
  ],
  address: [{
    line:       ['Storgata 1'],
    city:       'Oslo',
    postalCode: '0155',
    country:    'NO'
  }]
});

console.log('Patient ID:', patient.id);

// Search for patients
const results = await oystehr.fhir.search({
  resourceType: 'Patient',
  params: [
    { name: 'family', value: 'Hansen' },
    { name: 'birthdate', value: '1985-03-15' }
  ]
});

const patients = results.entry?.map(e => e.resource as Patient) ?? [];

Scheduling Appointments

TYPESCRIPT
// Get available slots
const slots = await oystehr.fhir.search({
  resourceType: 'Slot',
  params: [
    { name: 'schedule.actor',  value: `Practitioner/${practitionerId}` },
    { name: 'status',          value: 'free' },
    { name: 'start',           value: 'ge2026-04-20' },
    { name: 'start',           value: 'le2026-04-27' }
  ]
});

// Book an appointment
const appointment = await oystehr.fhir.create({
  resourceType: 'Appointment',
  status:       'booked',
  serviceType:  [{ coding: [{ system: 'http://snomed.info/sct', code: '11429006', display: 'Consultation' }] }],
  start:        '2026-04-22T10:00:00+02:00',
  end:          '2026-04-22T10:30:00+02:00',
  slot:         [{ reference: `Slot/${slotId}` }],
  participant: [
    {
      actor:  { reference: `Patient/${patientId}` },
      status: 'accepted'
    },
    {
      actor:  { reference: `Practitioner/${practitionerId}` },
      status: 'accepted'
    }
  ]
});

Recording Observations (Vitals)

TYPESCRIPT
// Record blood pressure
await oystehr.fhir.create({
  resourceType:  'Observation',
  status:        'final',
  category: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'vital-signs' }] }],
  code: { coding: [{ system: 'http://loinc.org', code: '85354-9', display: 'Blood pressure panel' }] },
  subject:       { reference: `Patient/${patientId}` },
  encounter:     { reference: `Encounter/${encounterId}` },
  effectiveDateTime: new Date().toISOString(),
  component: [
    {
      code: { coding: [{ system: 'http://loinc.org', code: '8480-6', display: 'Systolic BP' }] },
      valueQuantity: { value: 120, unit: 'mmHg', system: 'http://unitsofmeasure.org', code: 'mm[Hg]' }
    },
    {
      code: { coding: [{ system: 'http://loinc.org', code: '8462-4', display: 'Diastolic BP' }] },
      valueQuantity: { value: 80, unit: 'mmHg', system: 'http://unitsofmeasure.org', code: 'mm[Hg]' }
    }
  ]
});

Telehealth Integration

TYPESCRIPT
// Create a telehealth appointment — Oystehr manages the video room
const telehealthAppt = await oystehr.telehealth.createAppointment({
  patientId,
  practitionerId,
  startTime: '2026-04-22T14:00:00+02:00',
  duration:  30  // minutes
});

// Patient join link
const patientLink = telehealthAppt.joinUrls.patient;

// Provider join link
const providerLink = telehealthAppt.joinUrls.provider;

Document References (Clinical Notes)

TYPESCRIPT
// Attach a clinical note to an encounter
const encoded = Buffer.from(clinicalNoteText).toString('base64');

await oystehr.fhir.create({
  resourceType: 'DocumentReference',
  status:       'current',
  type: { coding: [{ system: 'http://loinc.org', code: '11488-4', display: 'Consult note' }] },
  subject:    { reference: `Patient/${patientId}` },
  context:    { encounter: [{ reference: `Encounter/${encounterId}` }] },
  date:       new Date().toISOString(),
  content: [{
    attachment: {
      contentType: 'text/plain',
      data:        encoded,
      title:       'Consultation Note 2026-04-22'
    }
  }]
});

Key Takeaways

  1. Oystehr is a headless EHR — you own the UI, they own the clinical data infrastructure and compliance
  2. Everything is a FHIR R4 resource — learn the core resources (Patient, Appointment, Encounter, Observation) and FHIR search
  3. HIPAA compliance is built in — Oystehr provides the BAA, audit logs, and secure infrastructure
  4. Standard FHIR code systems (LOINC, SNOMED, ICD-10) are used for interoperability — don't invent your own codes
  5. Use M2M tokens for server-side calls; SMART on FHIR for patient-facing apps that access patient data directly

Enjoyed this article?

Explore the learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.