May 19, 2026 · 10 min read

Headless EHR: What It Actually Takes to Build the Patient Side

Most telemedicine practices don't decide to run a headless EHR. They back into one. You pick a clinician-facing EHR, you find out new patients have nowhere to onboard themselves, and you start duct-taping a patient experience out of no-code automations. It holds together fine. Then a webhook quietly dies, onboarding is down for two days, and you realize you've built a second EHR nobody is maintaining. Here's the deliberate version, plus the platform behavior nobody writes down.

If you run a multi-state telemedicine practice, your electronic health record was built for clinicians. It schedules visits, holds charts, runs billing, handles prescribing. What it doesn't do is give a brand-new patient a way to find you, pick a plan, pay, and book a first visit without a staff member in the loop.

So you filled the gap yourself. A booking page here, a Zapier or n8n workflow there, a script that shuttles data between two systems, maybe a prototype some AI tool spat out in an afternoon. Every piece solved a real problem. Put together, they're a second EHR now, the one your patients actually touch. And unlike the first one, nobody's maintaining it on purpose.

That second system has a name. It's a headless EHR. Whether you run one well or quietly drown in it comes down to a single thing: did you build it on purpose, or did it happen to you.


You didn't choose a headless EHR. You backed into one.

Search "headless EHR" and the results frame it as a platform decision. API-first products like Healthie, Medplum, or Oystehr that ship a backend and leave the interface to you. That's accurate. It's also the vendor's side of the story.

The practice side looks different. You never shopped for "headless." You're on a normal EHR. Usually Healthie, which is ONC-certified, API-first, and used by more than 34,000 clinicians. And it doesn't ship the surfaces your patients need, so you built them. The moment a patient-facing app started talking to that EHR's API, you were running a headless architecture. Decision or no decision.

The architecture in one breath: the EHR stays the system of record for clinical data, billing, and prescribing, plus the portal a patient uses once they're activated. You own everything the patient touches before and around that, discovery, onboarding, plan selection, payment, the first booking. So the question was never whether to go headless. You already did, by accident. The real question is how to make it deliberate.


Why the no-code patient layer breaks

No-code automation tools are genuinely good at gluing two systems together. For a while. They stop being good the day the glue turns load-bearing, and here's how that comes apart.

  • Webhooks drift. A subscription disables itself after a run of failed deliveries, and nothing tells you. Onboarding can be down for days before someone notices new patients stopped getting confirmations.
  • No type safety, no tests. A field gets renamed upstream and a workflow breaks in silence. You hear about it from a patient, not from a failing test.
  • Secrets sprawl. API keys hardcoded across dozens of separate workflows. Rotating one key cleanly turns into a half-day archaeology dig.
  • Execution limits. No-code platforms cap runs per month. An error storm burns the quota, and then every automation in the practice stalls at once.
  • Duplicated logic. The same provider-matching rule is copy-pasted across four workflows. Fix a bug in one and you've still got three. You'll miss one.

This isn't the same failure as a vibecoded prototype, though the two rhyme. A vibecoded app is one system that was never production-grade to begin with, its own kind of 80/20 problem. Automation sprawl is the opposite shape. Dozens of small pieces, each fine on its own, that together have no owner, no tests, and no audit trail. Both land in the same place: the thing your business runs on can't be changed without holding your breath.


What a headless layer actually needs

Doing it on purpose means one codebase you own, not thirty workflows you tiptoe around. The EHR still stays the system of record. What you build and own is the integration layer wrapped around it.

Concern No-code sprawl One owned codebase
EHR API calls Untyped, breaks silently on a schema change Typed client; a renamed field is a compile error
Webhooks Disable silently, nobody notices Signed receiver plus a daily health check that alerts
Time-delayed work Polling, which burns execution quota A real job queue with retries
Audit trail None, or logs scattered everywhere Every external call recorded, end to end
Who can change it Whoever remembers how all 30 pieces fit Any engineer, because it's typed and tested

The audit log deserves its own line. In a regulated practice it isn't optional, and it's the first thing a rushed build quietly drops, because patients never see it. It's also the fix for the webhook outage from earlier. When every external call is on record, you can see what fired, what didn't, and what to replay. We've written separately on implementing audit trails for compliance.

None of this is fancy. That's deliberate. A small, ordinary, typed, tested stack is what one engineer can hold in their head, and what survives the person who built it moving on.


The Healthie footguns nobody puts in the docs

Healthie is the EHR we see most often under these builds, so let's get specific. What follows is general platform behavior, the kind anyone integrating Healthie's GraphQL API runs into. None of it lives on a tidy "gotchas" page. Each one has burned someone in production.

The primary provider is whoever owns the API key

Create an appointment in Healthie and the primary provider gets assigned implicitly. It's whoever owns the API key you authenticated with. There's no field on the mutation to override it, and the plural providers argument only tacks on secondaries. So if you call the create-appointment mutation with your org admin's key, every patient gets booked with the admin as their primary provider and the treating clinician as a footnote. The fix isn't hard: mint a separate API key per clinician, authenticate as the right one for each booking. Miss it, and you've mis-attributed months of visits before anyone notices.

Applying a tag charges the patient

Healthie practices commonly wire tags to package billing. Apply a tag, a charge fires. That makes tags dangerous in a way that isn't obvious until it bites. Any automation that uses tags for routing or metadata, which is the easy lazy choice, has quietly become a billing engine. So the rule is absolute: never apply a tag from automation unless you mean to charge the patient. For routing and state, reach for appointment types, custom fields, or chart notes. Never tags.

Webhooks tell you almost nothing, and turn themselves off quietly

A Healthie webhook payload gives you an ID, an event type, and a list of changed field names. Not the record itself. You re-fetch the full object by ID every single time, and even then, a webhook firing doesn't prove the operation actually succeeded. There's no dedicated event for an appointment being cancelled, completed, or marked no-show either. They all show up as a generic "updated" event, and you read the changed fields to work out what happened. Worst of all, webhooks quietly disable themselves after a run of failed deliveries. That's the whole reason the daily health check above isn't a nice-to-have.

Silent no-ops and recurring appointments

Two smaller traps. Recurring appointments don't emit webhooks by default, so you have to ask the vendor to switch that on. And at least one mutation wants a comma-terminated string where you'd expect a plain value, then silently does nothing if you leave the comma off, while still returning success. Silent no-ops are the worst kind of bug. The test that should catch them passes.


Payment-gating: the single highest-return fix

If a duct-taped patient layer leaks money anywhere, it leaks here. The naive booking flow creates the appointment first, then charges the card. Charge fails? You're left with a booked visit, a calendar hold, a video link, and no payment. The patient shows up, a clinician spends the time, the practice eats it. At any real volume, that's money walking out the door every week.

The fix is an ordering change. It has to be deliberate:

  1. Soft-hold the slot for a few minutes, so two patients can't grab it at once.
  2. Charge the card.
  3. Create the appointment only once the charge comes back confirmed.
  4. If the charge fails, release the hold, show an inline retry, and create nothing.

Obvious on paper. Genuinely hard in practice, because the failure cases are all concurrency and idempotency: a webhook landing mid-charge, a retry firing twice, a patient mashing the submit button. This is the same discipline a real payments system needs, and it's the exact part a cheap bid underestimates.

One detail specific to EHR billing. If your EHR has its own billing layer, and Healthie does, wired through Stripe, charge through it. Run a parallel Stripe charge on the side and you'll double-bill the patient and break the EHR's books.


When to rebuild, and how to phase it

You don't rebuild because the no-code stack is ugly. Ugly is fine. You rebuild when it's load-bearing and you can't change it safely anymore. The signal is concrete. A webhook outage nobody caught for a day. A renamed field that broke onboarding. A patient charged twice. Once you can't touch the patient experience without flinching, the duct tape already costs more than owning the thing would.

The mistake at that point is rebuilding everything at once. A multi-phase patient lifecycle doesn't need a six-figure big-bang. Phase it. Start with the slice that kills the most risk and recovers the most money, which is almost always the payment-gated booking funnel plus the foundations the rest sits on: the typed API client, the signed webhook receiver, the audit log. Ship that. Watch it work in production. Then decide the next phase on real numbers instead of a forecast. That's what a sane software rescue looks like, phased and evidence-first.

We build this kind of system for regulated healthcare. One was a DEA-regulated controlled-substance platform, immutable audit trail and compliance workflows included, taken from a broken codebase to a client-reviewable product in roughly 120 engineering hours. Another was a fintech compliance MVP, where we built the idempotent-billing backbone a payment gate lives or dies on, and where a double charge isn't an annoyance, it's a violation. Same pattern every time: a codebase you own, wrapped around a system-of-record API, with the money flow and the audit trail treated as the foundation rather than the polish.


Common questions

What is a headless EHR?

A headless EHR is an electronic health record where the clinical data, billing, and logic live in one API-driven system, and the user-facing experience is built separately on top of that API. In practice, most telemedicine practices don't buy a headless EHR. They run a standard EHR like Healthie and build their own patient-facing surfaces against its API, which makes the architecture headless whether or not anyone called it that.

Can you build a custom patient onboarding experience on top of Healthie?

Yes. Healthie is API-first and ONC-certified, and it's designed to sit behind a custom front end. The practice owns the patient-facing surfaces, things like onboarding, booking, payment, and plan selection, and Healthie stays the system of record for clinical data, billing, and prescribing. The work is in the integration layer: a typed API client, a webhook receiver, payment-gating, and an audit log.

Why do telemedicine practices replace n8n or Zapier workflows with custom code?

No-code automation works well for gluing two systems together for a while. It breaks down once the glue becomes load-bearing: webhooks silently disable, there are no tests, secrets are scattered across dozens of workflows, monthly execution limits stall everything during an error storm, and the same logic gets duplicated in places nobody tracks. A single owned codebase replaces the sprawl with something typed, tested, observable, and safe to change.

What is the most common Healthie API mistake?

Creating appointments with the organization admin's API key. Healthie assigns the primary provider implicitly from the API key's owner, and there's no field to override it. The result is every appointment attributed to the admin instead of the treating clinician. The fix is to mint a separate API key per provider and authenticate as the correct one for each booking.

In a booking flow, should payment happen before or after creating the appointment?

Before. If the appointment is created first and the charge fails, the practice is left with a booked visit, a calendar hold, and no payment. The correct order is to soft-hold the slot, charge the card, and create the appointment only once payment is confirmed. A failed charge releases the hold and creates nothing. Getting this right is mostly about idempotency and concurrency, so retries and mid-flight webhooks can't double-charge or leave orphaned bookings.


If your practice is running on automations you're afraid to touch, the first useful step isn't a rebuild quote. It's a clear-eyed map: what you've got, what's load-bearing, which slice to fix first. We do that as its own piece of work, and it's worth having whether or not you build the rest with us. Book a call and we'll walk your stack together, or read how we work with healthcare practices.

Sources: Healthie public platform and developer documentation (gethealthie.com); n8n Cloud plan documentation; Oktopeak project experience across regulated healthcare and fintech builds. The Healthie API behaviors described here are general platform characteristics, not specific to any client.

Healthcare

Related Articles

View all Healthcare articles ➔

Book a Call