This Week in Dr. Ray Automation: Heidi Sync, Session API, and 9 New Services
A massive week of launches: bidirectional Heidi-to-MN sync, a new Session API with audit capabilities, Clarius DynamoDB migration, and 9 new microservices.
This week was arguably the most productive of the quarter. We shipped 12 new or significantly updated services across the Dr. Ray automation stack — ranging from a brand-new bidirectional sync between Heidi Health and Maternity Neighborhood, to an entirely new Session API with audit capabilities, to infrastructure migrations that set us up for the next phase of growth.
Heidi → Maternity Neighborhood Sync
The marquee launch this week: mn-heidi-sync, a CLI tool that syncs Heidi Health clinical sessions directly into Maternity Neighborhood (MN) as encounters.
Why this matters
Dr. Ray’s practice uses Heidi Health for clinical note-taking during patient visits. Maternity Neighborhood is the practice management system that handles billing, scheduling, and patient records. Until now, these two systems lived in parallel — staff were manually re-entering session data into MN after each visit.
What mn-heidi-sync does:
- Pulls sessions from Heidi via their API
- Deduplicates against existing MN encounters (by date + patient name fuzzy matching)
- Creates new encounters in MN with the consult notes, timestamps, and metadata
- Runs as a CLI with
--cronmode for automated nightly runs - Includes a QA verification tool (
qa.js+verify.sh) for data integrity checks
# Run manually
node sync.js
# Cron mode (webhook-only, no file saving)
node sync.js --cron
# Dry run
node sync.js --dry-run
Duplicate guarding
The trickiest part of any sync is avoiding duplicates. We implemented two layers:
- Local guard: Check DynamoDB for
progressNoteContains+listProgressNotesbefore creating - MN-side guard: Query MN’s API for existing encounters on the same date before
addEncounter
Both checks use fuzzy name matching (Levenshtein distance + tokenization) to handle variations like “Julia Ray” vs “Dr. Julia Ray” vs “Ray, Julia”.
Reverse-engineering the MN API
Maternity Neighborhood doesn’t have a public API. So we built one.
The approach:
We fired up Chrome, opened DevTools, and used the Chrome DevTools MCP to capture every network request the browser made when creating an encounter. This gave us the exact form structure, headers, and endpoints needed to replicate the browser’s behavior programmatically.
What we found:
- Login flow: Requires XSRF token obtained from
/sterling/session/(returns 401, but sets the cookie). Login POST to/sterling/session/login/with JSON body +X-XSRF-TOKENheader +Origin/Refererheaders. - Add encounter: POST to
/episodes/{episodeId}/progress_notes/add/with multipart form data - Form structure: A massive form — 40+ form fields covering everything from physical exam (heent, chest, abdomen, extremities, skin) to antenatal data (menstrual history, problems list, genetic screens, conception details)
// Form fields captured from browser
{
'antenatal-currpreg-problems_values': { ... },
'antenatal-currpreg-tests_screens_values': { ... },
'antenatal-currpreg-conception_values': { ... },
'physical-genitourinary_internal_values': { ... },
'physical-abdominal_back_values': { ... },
'csrf': '61724dfa936f5970074f7d041fd500edbfc2d88a',
'service_date': '02/19/2026',
// ... 30+ more fields
}
The draft problem:
Early on, we hit a wall: encounters created via the API appeared as drafts in the MN UI, despite getting HTTP 200 responses. We spent significant time reverse-engineering why:
- Hypothesis 1: Empty
autosave.idfield — tested, didn’t fix it - Hypothesis 2: Missing
sec-fetch-*headers — tested, didn’t fix it - Hypothesis 3: Field order differences — still investigating
The root cause is still being debugged, but the sync still works — drafts can be finalized manually, and the data makes it into MN.
No JSON API for progress notes:
Here’s the kicker: MN has no JSON endpoint for listing progress notes. We had to scrape the HTML list page (GET .../episodes/{id}/progress_notes/) and search the response body for session IDs to detect duplicates.
// Duplicate guard: fetch HTML, search for session ID
async progressNoteContains(episodeId, sessionId) {
const html = await this.ehrFetch(`/episodes/${episodeId}/progress_notes/`);
return html.includes(`Session ID: ${sessionId}`);
}
This is the ugly side of reverse-engineering: sometimes you resort to regex-matching HTML because that’s all the API gives you.
Heidi Session API
We launched a new Express API that wraps the Heidi Sessions data with additional capabilities:
Audit mode
The Session API now exposes an auditSessions endpoint that compares Heidi sessions against Google Calendar to flag suspicious data:
- Bad Name: Patient name doesn’t match any calendar event (potential typo or test data)
- No Calendar Match: Session exists but no corresponding calendar entry found
This uses fuzzy matching across both the session name and calendar event name.
Batch name updates
If a patient updates their name (marriage, legal change, etc.), the API supports updatePatientName — a batch update that changes the patient name across all their sessions and triggers re-embedding for semantic search.
Timezone-aware dates
Heidi timestamps come without timezone info. The API now normalizes all dates using America/Los_Angeles (Dr. Ray’s timezone) and stores a localDate field for reliable calendar matching.
Heidi Explorer UI
A new React app built this week for exploring and auditing Heidi sessions:
- Session Review mode: Compare session discrepancies vs Google Calendar
- AuditSessionCard: Visual flags for sessions with bad names or no calendar match
- Inline patient name editing: Update a patient’s name across all sessions directly from the UI
- Fixed StrictMode double-API-call bug: Reduced initial load from 6 API calls to 3
Clarius Scraper: DynamoDB Migration
The Clarius ultrasound scraper got a significant backend overhaul:
What’s new
- DynamoDB migration: Moved from
exam-ids.jsonfile storage to theClariusExamstable in DynamoDB - Catch-up pagination: Scrapes up to 3 pages on each run, stops when hitting an already-processed exam
- Reserved keyword fix: Used
ExpressionAttributeNamesfor theprocessedfield (DynamoDB reserved word) - Bundled upload-fax.sh: The script now lives in the repo so it works on VPS without depending on the parent drray directory
# Resync + PDF generation
./resync.sh
# Dry run
./resync.sh --dry-run
New Microservices
New services launched or initialized this week:
| Service | Description |
|---|
| heidi-session-api | Express API for Heidi sessions with audit/calendar matching | | search-calendar | Calendar search service |
Stripe Installment Updates
The pricing calculator and Stripe handler got love too:
- Custom installment option: Patients can now select custom installment plans
- Finalize 15 days before due date: Installments now finalize 15 days before the selected payment date (was previously same-day)
- Test mode badge: Visual indicator when running against Stripe staging
- Generic go-back navigation: Improved UX flow
Weekly Commit Digest
One more thing — I wrote a digest script that generates a weekly commit summary across all 70 subdirectory repositories in the drray monorepo-coordination repo:
# Daily digest (last 24 hours)
./digest
# Weekly digest (last 7 days)
./digest weekly
./digest w
This week: 54 commits across 23 repositories. The full digest is available in the drray repo.