How We Compared Telehealth vs In-Person PMHNP Pay Across 10,000+ Job Posts
A data-engineering look at salary normalization, deduplication, and why remote roles often price higher than “easier work.”
“Telehealth pays less” is usually a conclusion drawn from one offer. When you aggregate thousands of postings and normalize comp structures, the pattern flips: telehealth often pays more.
The myth: telehealth is “easier,” so it pays less
On PMHNP Hiring we ingest 500+ sources daily and maintain 10,000+ verified PMHNP jobs across all 50 states. When we look at compensation across that dataset (after normalizing salary formats and removing duplicates), the common claim that in-person always pays more doesn’t hold up.
Across postings that include usable pay data, telehealth roles often price higher than in-person roles.
That doesn’t mean every remote job beats every onsite job. It means the distribution is different enough that treating telehealth as a “pay cut for flexibility” is a bad default.
This post is the builder’s version of the question: what does the data say, and what did we have to do technically to make it comparable?
Why pay comparisons are hard (and why raw job boards mislead)
Job posts rarely ship “clean” salary fields. The same compensation can show up as:
$140/hr(W2 hourly)$1,200/day$250/visit(1099)Up to $220k(base + bonus unknown)80% collections(requires assumptions)
If you compare those strings directly, you’ll produce nonsense. Our pipeline has to:
- Extract comp from messy text (structured fields when available, otherwise description parsing)
- Normalize to comparable units (hourly ↔ annual, ranges ↔ midpoint)
- Classify pay model (salary, hourly, per-visit, RVU/collections)
- Deduplicate cross-posted roles so one high-paying listing doesn’t appear 30 times
- Segment by modality (telehealth vs in-person vs hybrid) using both metadata and text signals
Only after that do “telehealth vs in-person” comparisons become meaningful.
The pipeline: from scraped postings to comparable numbers
At a high level, we treat each source as an input adapter that maps into a common schema, then run enrichment steps.
1) Canonical job schema
We store a normalized representation (Supabase/Postgres), keeping raw fields for debugging:
type PayModel = 'salary' | 'hourly' | 'per_visit' | 'rvu' | 'collections' | 'unknown'
type Job = {
id: string
source: string
source_job_id: string
title: string
company: string
location_text: string
remote_type: 'telehealth' | 'in_person' | 'hybrid' | 'unknown'
pay_model: PayModel
pay_min?: number
pay_max?: number
pay_unit?: 'year' | 'hour' | 'visit'
pay_currency?: 'USD'
description: string
posted_at: string
fingerprint: string // for dedupe
}
2) Salary parsing + normalization
We normalize into an annualized estimate only when the pay model supports it. For hourly W2 roles, annualization is straightforward (with assumptions). For per-visit/collections, we keep the model explicit to avoid inventing certainty.
const HOURS_PER_YEAR = 2080
function annualize(job: Job) {
if (job.pay_model === 'hourly' && job.pay_unit === 'hour') {
return {
annual_min: job.pay_min ? job.pay_min * HOURS_PER_YEAR : null,
annual_max: job.pay_max ? job.pay_max * HOURS_PER_YEAR : null,
confidence: 'medium',
}
}
if (job.pay_model === 'salary' && job.pay_unit === 'year') {
return {
annual_min: job.pay_min ?? null,
annual_max: job.pay_max ?? null,
confidence: 'high',
}
}
// per-visit / collections / RVU require volume assumptions → do not annualize by default
return { annual_min: null, annual_max: null, confidence: 'low' }
}
This is where a lot of “telehealth pays less” myths come from: many remote roles are posted as per-visit or production-based, while hospital roles are posted as clean annual salaries. If you only compare annual-salary postings, you bias toward in-person systems.
3) Deduplication (the hidden salary inflation bug)
High-volume telehealth platforms syndicate aggressively. Without dedupe, your dataset overcounts the same role and skews pay stats.
We generate a fingerprint from stable fields (company + title + state/license requirement + pay band + remote type) and cluster near-matches.
Architecture note: dedupe is a blend of deterministic hashing + fuzzy matching (string similarity on company/title) with thresholds tuned by manual review.
What the data shows: telehealth often prices higher
After normalization and dedupe, we compare distributions by remote_type, segmented by pay model (salary vs hourly vs per-visit).
A simplified SQL sketch:
select
remote_type,
pay_model,
percentile_cont(0.5) within group (order by annual_mid) as p50,
count(*) as n
from (
select
remote_type,
pay_model,
case
when annual_min is not null and annual_max is not null then (annual_min + annual_max)/2
when annual_min is not null then annual_min
when annual_max is not null then annual_max
else null
end as annual_mid
from job_comp_normalized
where annual_min is not null or annual_max is not null
) x
group by 1,2
order by n desc;
The repeated pattern we see:
- Telehealth salary/hourly postings often cluster higher than comparable in-person postings.
- The gap gets bigger in roles that signal urgency: multi-state licensing, nights/weekends, fast start dates.
- In-person still wins in specific slices: hospital systems with strong benefits and stable base salaries.
So why does telehealth price higher so often?
The business math behind the pay (as seen through job post signals)
From a data standpoint, remote roles correlate with signals that predict higher comp:
Competition is national, not local
- Telehealth employers compete against other remote-first orgs. We see faster repost cycles and higher pay edits in these listings.
Many remote models are throughput-optimized
- Posts mention standardized workflows, shorter appointment gaps, and reduced no-shows. That tends to pair with productivity pay or higher hourly rates.
Coverage + urgency premiums
- Remote roles disproportionately include nights/weekends, rural coverage, and “licensed in X state” requirements.
Technically, these show up as text features we can index and filter: weekend, after-hours, multi-state, compact, ASAP, etc. They’re not perfect, but they’re strong enough to segment on.
What we surface in the product (and why it matters for negotiation)
On the UI side (Next.js + TypeScript), we expose filters that map directly to the normalized schema:
- Telehealth / in-person / hybrid
- Pay model (salary vs hourly vs per-visit)
- Pay range (only when confidence is sufficient)
- State licensing requirements
Alerts (email/push) are triggered when new postings match saved filters, so users can watch their slice of the market rather than relying on anecdotes.
If you’re negotiating, the practical takeaway is data-driven: don’t assume telehealth implies a discount. Treat modality as one variable, then compare roles with the same pay model and similar constraints.
Next up: improving apples-to-apples comparisons
The hardest remaining problem is per-visit and collections-based comp. We’re working on “expected annual comp” estimates by pairing postings with realistic volume assumptions (and clearly labeling them as assumptions). That’s the only way to compare a $250/visit role against a $190k base role without hand-waving.