From 4b0155542e89f35f2c026c256541e38f0b802e80 Mon Sep 17 00:00:00 2001 From: justin Date: Fri, 5 Jun 2026 01:33:36 -0500 Subject: [PATCH] feat(npi): healthcare marketing pages, nav dropdown, NPI lookup API + free tool + companion data migration/loader --- api/migrations/088_npi_compliance_data.sql | 69 ++++ api/src/index.ts | 2 + api/src/routes/npi-lookup.ts | 361 ++++++++++++++++++ scripts/load_npi_companion_data.py | 168 ++++++++ .../src/pages/services/healthcare/index.astro | 94 +++++ .../healthcare/medicare-enrollment.astro | 69 ++++ .../healthcare/npi-revalidation.astro | 84 ++++ .../pages/tools/npi-compliance-check.astro | 327 ++++++++++++++++ site/src/partials/nav.html | 4 +- 9 files changed, 1176 insertions(+), 2 deletions(-) create mode 100644 api/migrations/088_npi_compliance_data.sql create mode 100644 api/src/routes/npi-lookup.ts create mode 100644 scripts/load_npi_companion_data.py create mode 100644 site/src/pages/services/healthcare/index.astro create mode 100644 site/src/pages/services/healthcare/medicare-enrollment.astro create mode 100644 site/src/pages/services/healthcare/npi-revalidation.astro create mode 100644 site/src/pages/tools/npi-compliance-check.astro diff --git a/api/migrations/088_npi_compliance_data.sql b/api/migrations/088_npi_compliance_data.sql new file mode 100644 index 0000000..51752ce --- /dev/null +++ b/api/migrations/088_npi_compliance_data.sql @@ -0,0 +1,69 @@ +-- ───────────────────────────────────────────────────────────────────── +-- NPI / Healthcare provider compliance lookup data. +-- +-- Powers /tools/npi-compliance-check. The live identity/status comes from +-- the free public NPPES Registry API at query time; these tables hold the +-- *dateable hooks* and exclusion data that the public NPI API does NOT +-- expose, all keyed by NPI: +-- +-- npi_revalidation_due — CMS Medicare revalidation due dates (the flagship +-- dateable hook; ~218k providers are PAST DUE). +-- npi_exclusions — OIG LEIE exclusion list (matched by NPI when present). +-- npi_optout — Medicare opt-out affidavits (end dates). +-- +-- Source files (free, public): +-- data.cms.gov Revalidation Due List, Medicare Opt Out +-- oig.hhs.gov LEIE downloadable database +-- ───────────────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS npi_revalidation_due ( + id SERIAL PRIMARY KEY, + npi TEXT NOT NULL, + enrollment_id TEXT, + first_name TEXT, + last_name TEXT, + organization_name TEXT, + enrollment_state TEXT, + enrollment_type TEXT, -- CMS enrollment type code + provider_type TEXT, + specialty TEXT, + revalidation_due_date DATE, -- NULL = "TBD" in CMS data + adjusted_due_date DATE, + reassign_to TEXT, + receiving_reassignment TEXT, + loaded_at TIMESTAMPTZ DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_npi_reval_npi ON npi_revalidation_due (npi); + +CREATE TABLE IF NOT EXISTS npi_exclusions ( + id SERIAL PRIMARY KEY, + npi TEXT, -- '0000000000' / blank when OIG has none + last_name TEXT, + first_name TEXT, + middle_name TEXT, + business_name TEXT, + general_category TEXT, + specialty TEXT, + state TEXT, + exclusion_type TEXT, -- e.g. 1128a1 + exclusion_date DATE, + reinstatement_date DATE, -- NULL when still excluded + loaded_at TIMESTAMPTZ DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_npi_excl_npi ON npi_exclusions (npi) WHERE npi IS NOT NULL AND npi <> '0000000000'; + +CREATE TABLE IF NOT EXISTS npi_optout ( + id SERIAL PRIMARY KEY, + npi TEXT, + first_name TEXT, + last_name TEXT, + specialty TEXT, + optout_effective_date DATE, + optout_end_date DATE, + state TEXT, + loaded_at TIMESTAMPTZ DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_npi_optout_npi ON npi_optout (npi); + +-- Reuse the existing free-tool logging table (compliance_check_log) for NPI +-- checks; no schema change needed there. diff --git a/api/src/index.ts b/api/src/index.ts index d3066b3..a1739fb 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -47,6 +47,7 @@ import portalEsignGenericRouter from "./routes/portal-esign-generic.js"; import pucRouter from "./routes/puc.js"; import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js"; import dotLookupRouter from "./routes/dot-lookup.js"; +import npiLookupRouter from "./routes/npi-lookup.js"; import surveyRouter from "./routes/survey.js"; import orderTimelineRouter from "./routes/order-timeline.js"; @@ -122,6 +123,7 @@ app.use(foreignQualRouter); app.use(pucRouter); app.use(fccCarrierRegRouter); app.use(dotLookupRouter); +app.use(npiLookupRouter); app.use(surveyRouter); app.use(orderTimelineRouter); app.use(adminCryptoRouter); diff --git a/api/src/routes/npi-lookup.ts b/api/src/routes/npi-lookup.ts new file mode 100644 index 0000000..eddb02a --- /dev/null +++ b/api/src/routes/npi-lookup.ts @@ -0,0 +1,361 @@ +/** + * NPI / Healthcare provider compliance lookup. + * + * GET /api/v1/npi/lookup?npi=1234567893 + * GET /api/v1/npi/search?name=Acme+Clinic&state=CA + * + * Combines the free public NPPES Registry API (live identity + status) with + * local CMS/OIG companion tables (revalidation due dates, exclusions, opt-out) + * to produce a green/yellow/red compliance summary, mirroring dot-lookup.ts. + */ + +import { Router } from "express"; +import { pool } from "../db.js"; + +const router = Router(); +const NPPES_BASE = "https://npiregistry.cms.hhs.gov/api/"; + +// ── Helpers ───────────────────────────────────────────────────────── + +async function nppesFetch(params: Record): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 6000); + try { + const qs = new URLSearchParams({ version: "2.1", ...params }).toString(); + const resp = await fetch(`${NPPES_BASE}?${qs}`, { + signal: controller.signal, + headers: { Accept: "application/json" }, + }); + clearTimeout(timer); + if (!resp.ok) return null; + return await resp.json(); + } catch { + clearTimeout(timer); + return null; + } +} + +type CheckStatus = "green" | "yellow" | "red" | "unknown"; + +interface ComplianceCheck { + id: string; + label: string; + status: CheckStatus; + detail: string; + action_url?: string | null; +} + +function daysBetween(a: Date, b: Date): number { + return Math.round((a.getTime() - b.getTime()) / 86400000); +} + +function fmtDate(d: Date | null): string { + if (!d) return "—"; + return d.toISOString().slice(0, 10); +} + +function nppesName(basic: any): string { + if (!basic) return "Unknown provider"; + if (basic.organization_name) return basic.organization_name; + const parts = [basic.first_name, basic.middle_name, basic.last_name].filter(Boolean); + const name = parts.join(" ").trim(); + return name || basic.name || "Unknown provider"; +} + +// ── Lookup by NPI ─────────────────────────────────────────────────── + +router.get("/api/v1/npi/lookup", async (req, res) => { + const rawNpi = String(req.query.npi || "").replace(/\D/g, ""); + if (!/^\d{10}$/.test(rawNpi)) { + res.status(400).json({ error: "Provide a valid 10-digit NPI." }); + return; + } + + const startedAt = Date.now(); + + try { + // 1) Live NPPES identity + status + const nppes = await nppesFetch({ number: rawNpi }); + const result = nppes?.results?.[0] || null; + + if (!result) { + res.status(404).json({ + error: "NPI not found in NPPES.", + npi: rawNpi, + }); + return; + } + + const basic = result.basic || {}; + const name = nppesName(basic); + const status = basic.status || null; // "A" active, "D"/null = deactivated + const enumType = result.enumeration_type || null; // NPI-1 individual, NPI-2 org + const lastUpdated = basic.last_updated ? new Date(basic.last_updated) : null; + const enumerationDate = basic.enumeration_date ? new Date(basic.enumeration_date) : null; + const deactivationDate = basic.deactivation_date ? new Date(basic.deactivation_date) : null; + + const primaryTaxonomy = (result.taxonomies || []).find((t: any) => t.primary) || (result.taxonomies || [])[0] || null; + const locationAddr = (result.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (result.addresses || [])[0] || null; + const practiceState = locationAddr?.state || null; + + // 2) Companion data joins (best-effort; tables may be empty pre-load) + const [revalRes, exclRes, optoutRes] = await Promise.all([ + pool.query( + `SELECT revalidation_due_date, adjusted_due_date, enrollment_type, specialty, enrollment_state + FROM npi_revalidation_due WHERE npi = $1 ORDER BY id LIMIT 1`, + [rawNpi] + ).catch(() => ({ rows: [] as any[] })), + pool.query( + `SELECT exclusion_type, exclusion_date, reinstatement_date, general_category + FROM npi_exclusions + WHERE npi = $1 AND npi <> '0000000000' + ORDER BY exclusion_date DESC NULLS LAST LIMIT 1`, + [rawNpi] + ).catch(() => ({ rows: [] as any[] })), + pool.query( + `SELECT optout_effective_date, optout_end_date, specialty + FROM npi_optout WHERE npi = $1 ORDER BY optout_end_date DESC NULLS LAST LIMIT 1`, + [rawNpi] + ).catch(() => ({ rows: [] as any[] })), + ]); + + const reval = revalRes.rows[0] || null; + const excl = exclRes.rows[0] || null; + const optout = optoutRes.rows[0] || null; + + const now = new Date(); + const checks: ComplianceCheck[] = []; + + // ── Check 1: NPI active status ────────────────────────────────── + if (status === "A" && !deactivationDate) { + checks.push({ + id: "npi-active", + label: "NPI registration", + status: "green", + detail: "Your NPI is active in NPPES.", + action_url: null, + }); + } else { + checks.push({ + id: "npi-active", + label: "NPI registration", + status: "red", + detail: deactivationDate + ? `Your NPI was deactivated on ${fmtDate(deactivationDate)}. You cannot bill until it is reactivated.` + : "Your NPI is not active in NPPES. Reactivation is required before you can bill.", + action_url: "/order/npi-reactivation", + }); + } + + // ── Check 2: Medicare revalidation (the dateable hook) ────────── + if (reval) { + const due = reval.adjusted_due_date || reval.revalidation_due_date; + if (!due) { + checks.push({ + id: "revalidation", + label: "Medicare revalidation", + status: "yellow", + detail: "CMS lists you as enrolled but has not yet set a revalidation due date (\"TBD\"). We can monitor it for you.", + action_url: "/services/healthcare/npi-revalidation", + }); + } else { + const dueDate = new Date(due); + const daysToD = daysBetween(dueDate, now); + if (daysToD < 0) { + checks.push({ + id: "revalidation", + label: "Medicare revalidation", + status: "red", + detail: `Your Medicare revalidation was due ${fmtDate(dueDate)} — ${Math.abs(daysToD)} days ago. CMS may deactivate your billing privileges. File now.`, + action_url: "/order/npi-revalidation", + }); + } else if (daysToD <= 180) { + checks.push({ + id: "revalidation", + label: "Medicare revalidation", + status: "yellow", + detail: `Your Medicare revalidation is due ${fmtDate(dueDate)} — ${daysToD} days away. File before the deadline to avoid deactivation.`, + action_url: "/order/npi-revalidation", + }); + } else { + checks.push({ + id: "revalidation", + label: "Medicare revalidation", + status: "green", + detail: `Your next Medicare revalidation is due ${fmtDate(dueDate)}.`, + action_url: null, + }); + } + } + } else { + checks.push({ + id: "revalidation", + label: "Medicare revalidation", + status: "unknown", + detail: "We did not find an active Medicare revalidation record for this NPI. If you bill Medicare, confirm your enrollment status in PECOS.", + action_url: "/services/healthcare/medicare-enrollment", + }); + } + + // ── Check 3: OIG / SAM exclusion ──────────────────────────────── + if (excl && !excl.reinstatement_date) { + checks.push({ + id: "exclusion", + label: "OIG exclusion screening", + status: "red", + detail: `This NPI matches an active OIG exclusion (${excl.exclusion_type || "type unknown"}, excluded ${fmtDate(excl.exclusion_date ? new Date(excl.exclusion_date) : null)}). Excluded providers cannot bill federal healthcare programs.`, + action_url: "/order/oig-sam-screening", + }); + } else { + checks.push({ + id: "exclusion", + label: "OIG exclusion screening", + status: "green", + detail: "No active OIG exclusion found for this NPI. Annual screening of your staff is still recommended.", + action_url: "/order/oig-sam-screening", + }); + } + + // ── Check 4: NPPES record freshness ───────────────────────────── + if (lastUpdated) { + const staleDays = daysBetween(now, lastUpdated); + if (staleDays > 365 * 2) { + checks.push({ + id: "nppes-fresh", + label: "NPPES record currency", + status: "yellow", + detail: `Your NPPES record was last updated ${fmtDate(lastUpdated)} — over 2 years ago. CMS requires updates within 30 days of any change. Re-attest to keep it current.`, + action_url: "/order/nppes-update", + }); + } else { + checks.push({ + id: "nppes-fresh", + label: "NPPES record currency", + status: "green", + detail: `Your NPPES record was last updated ${fmtDate(lastUpdated)}.`, + action_url: null, + }); + } + } + + // ── Check 5: Medicare opt-out ─────────────────────────────────── + if (optout && optout.optout_end_date) { + const endDate = new Date(optout.optout_end_date); + const daysToEnd = daysBetween(endDate, now); + if (daysToEnd >= 0 && daysToEnd <= 365) { + checks.push({ + id: "optout", + label: "Medicare opt-out", + status: "yellow", + detail: `Your Medicare opt-out affidavit ends ${fmtDate(endDate)}. Opt-out auto-renews every 2 years unless you act; if you want to re-enroll, plan ahead.`, + action_url: "/services/healthcare/medicare-enrollment", + }); + } + } + + const redCount = checks.filter((c) => c.status === "red").length; + const yellowCount = checks.filter((c) => c.status === "yellow").length; + const greenCount = checks.filter((c) => c.status === "green").length; + const severity = redCount > 0 ? "critical" : yellowCount > 0 ? "major" : "clean"; + + // Fire-and-forget logging (reuse compliance_check_log; NPI stored in frn col) + pool.query( + `INSERT INTO compliance_check_log + (frn, entity_name, ip_address, user_agent, referrer, total_checks, issues_found, severity, check_slugs, response_ms) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, + [ + rawNpi, + name, + (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() || req.ip || null, + req.headers["user-agent"] || null, + req.headers["referer"] || null, + checks.length, + redCount + yellowCount, + severity, + checks.filter((c) => c.status === "red" || c.status === "yellow").map((c) => c.id), + Date.now() - startedAt, + ] + ).catch(() => {}); + + res.json({ + npi: rawNpi, + name, + enumeration_type: enumType, + status, + taxonomy: primaryTaxonomy + ? { code: primaryTaxonomy.code, desc: primaryTaxonomy.desc, state: primaryTaxonomy.state, license: primaryTaxonomy.license } + : null, + practice_state: practiceState, + enumeration_date: enumerationDate ? fmtDate(enumerationDate) : null, + last_updated: lastUpdated ? fmtDate(lastUpdated) : null, + checks, + summary: { + red: redCount, + yellow: yellowCount, + green: greenCount, + total: checks.length, + }, + severity, + checked_at: new Date().toISOString(), + }); + } catch (err) { + console.error("[npi-lookup] Error:", err); + if (!res.headersSent) res.status(500).json({ error: "NPI lookup failed." }); + } +}); + +// ── Name search ───────────────────────────────────────────────────── + +router.get("/api/v1/npi/search", async (req, res) => { + const name = String(req.query.name || "").trim(); + const state = String(req.query.state || "").trim().toUpperCase(); + if (name.length < 2) { + res.status(400).json({ error: "Provide a name (at least 2 characters)." }); + return; + } + + try { + // NPPES supports org name and last name search; try organization first, + // then individual last name. Wildcard with trailing * for partials. + const params: Record = { limit: "15" }; + if (state && state.length === 2) params.state = state; + + // Heuristic: single token -> last_name; otherwise org name. + const tokens = name.split(/\s+/); + if (tokens.length === 1) { + params.last_name = name + "*"; + } else { + params.organization_name = name + "*"; + } + + let nppes = await nppesFetch(params); + // Fallback: if org search empty, try last name + if (!nppes?.results?.length && !params.last_name) { + delete params.organization_name; + params.last_name = tokens[tokens.length - 1] + "*"; + nppes = await nppesFetch(params); + } + + const results = (nppes?.results || []).map((r: any) => { + const b = r.basic || {}; + const loc = (r.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (r.addresses || [])[0] || {}; + const tax = (r.taxonomies || []).find((t: any) => t.primary) || (r.taxonomies || [])[0] || {}; + return { + npi: r.number, + name: nppesName(b), + enumeration_type: r.enumeration_type, + status: b.status, + taxonomy: tax.desc || null, + city: loc.city || null, + state: loc.state || null, + }; + }); + + res.json({ query: name, count: results.length, results }); + } catch (err) { + console.error("[npi-search] Error:", err); + if (!res.headersSent) res.status(500).json({ error: "NPI search failed." }); + } +}); + +export default router; diff --git a/scripts/load_npi_companion_data.py b/scripts/load_npi_companion_data.py new file mode 100644 index 0000000..ba2d766 --- /dev/null +++ b/scripts/load_npi_companion_data.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Load CMS/OIG NPI companion data into Postgres for the NPI compliance check. + +Populates: + npi_revalidation_due <- CMS Revalidation Due List + npi_exclusions <- OIG LEIE + npi_optout <- CMS Medicare Opt Out + +Usage: + DATABASE_URL=postgresql://... python3 scripts/load_npi_companion_data.py \ + --dir /tmp/npi_companion + +Source CSVs (free/public): + revalidation_due.csv data.cms.gov Medicare Revalidation Due List + leie.csv oig.hhs.gov LEIE downloadable database + optout.csv data.cms.gov Medicare Opt Out +""" +import argparse +import csv +import os +import sys +from datetime import datetime + +import psycopg2 +from psycopg2.extras import execute_values + +DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://pw:pw@localhost:5432/performancewest") + + +def parse_date(s): + if not s: + return None + s = s.strip() + if not s or s in ("00000000", "TBD"): + return None + for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%Y%m%d", "%m/%d/%y"): + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + return None + + +def clean_npi(s): + s = (s or "").strip() + return s if s and s != "0000000000" and len(s) == 10 and s.isdigit() else (s or None) + + +def load_revalidation(conn, path): + rows = [] + with open(path, newline="", encoding="utf-8-sig") as f: + for r in csv.DictReader(f): + npi = (r.get("National Provider Identifier") or "").strip() + if not (npi.isdigit() and len(npi) == 10): + continue + rows.append(( + npi, + (r.get("Enrollment ID") or "").strip() or None, + (r.get("First Name") or "").strip() or None, + (r.get("Last Name") or "").strip() or None, + (r.get("Organization Name") or "").strip() or None, + (r.get("Enrollment State Code") or "").strip() or None, + (r.get("Enrollment Type") or "").strip() or None, + (r.get("Provider Type Text") or "").strip() or None, + (r.get("Enrollment Specialty") or "").strip() or None, + parse_date(r.get("Revalidation Due Date")), + parse_date(r.get("Adjusted Due Date")), + (r.get("Individual Total Reassign To") or "").strip() or None, + (r.get("Receiving Benefits Reassignment") or "").strip() or None, + )) + with conn.cursor() as cur: + cur.execute("TRUNCATE npi_revalidation_due RESTART IDENTITY") + execute_values(cur, """ + INSERT INTO npi_revalidation_due + (npi, enrollment_id, first_name, last_name, organization_name, + enrollment_state, enrollment_type, provider_type, specialty, + revalidation_due_date, adjusted_due_date, reassign_to, receiving_reassignment) + VALUES %s + """, rows, page_size=5000) + conn.commit() + return len(rows) + + +def load_exclusions(conn, path): + rows = [] + with open(path, newline="", encoding="utf-8-sig") as f: + for r in csv.DictReader(f): + rows.append(( + clean_npi(r.get("NPI")), + (r.get("LASTNAME") or "").strip() or None, + (r.get("FIRSTNAME") or "").strip() or None, + (r.get("MIDNAME") or "").strip() or None, + (r.get("BUSNAME") or "").strip() or None, + (r.get("GENERAL") or "").strip() or None, + (r.get("SPECIALTY") or "").strip() or None, + (r.get("STATE") or "").strip() or None, + (r.get("EXCLTYPE") or "").strip() or None, + parse_date(r.get("EXCLDATE")), + parse_date(r.get("REINDATE")), + )) + with conn.cursor() as cur: + cur.execute("TRUNCATE npi_exclusions RESTART IDENTITY") + execute_values(cur, """ + INSERT INTO npi_exclusions + (npi, last_name, first_name, middle_name, business_name, + general_category, specialty, state, exclusion_type, + exclusion_date, reinstatement_date) + VALUES %s + """, rows, page_size=5000) + conn.commit() + return len(rows) + + +def load_optout(conn, path): + rows = [] + with open(path, newline="", encoding="utf-8-sig") as f: + for r in csv.DictReader(f): + npi = (r.get("npi") or r.get("NPI") or "").strip() + if not (npi.isdigit() and len(npi) == 10): + continue + rows.append(( + npi, + (r.get("First Name") or "").strip() or None, + (r.get("Last Name") or "").strip() or None, + (r.get("Specialty") or "").strip() or None, + parse_date(r.get("Optout Effective Date")), + parse_date(r.get("Optout End Date")), + (r.get("State Code") or "").strip() or None, + )) + with conn.cursor() as cur: + cur.execute("TRUNCATE npi_optout RESTART IDENTITY") + execute_values(cur, """ + INSERT INTO npi_optout + (npi, first_name, last_name, specialty, + optout_effective_date, optout_end_date, state) + VALUES %s + """, rows, page_size=5000) + conn.commit() + return len(rows) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dir", default="/tmp/npi_companion") + args = ap.parse_args() + + conn = psycopg2.connect(DATABASE_URL) + try: + jobs = [ + ("revalidation_due.csv", load_revalidation), + ("leie.csv", load_exclusions), + ("optout.csv", load_optout), + ] + for fname, fn in jobs: + path = os.path.join(args.dir, fname) + if not os.path.exists(path): + print(f" SKIP {fname} (not found at {path})") + continue + n = fn(conn, path) + print(f" loaded {n:,} rows from {fname}") + finally: + conn.close() + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/site/src/pages/services/healthcare/index.astro b/site/src/pages/services/healthcare/index.astro new file mode 100644 index 0000000..bbae69a --- /dev/null +++ b/site/src/pages/services/healthcare/index.astro @@ -0,0 +1,94 @@ +--- +import Base from "../../../layouts/Base.astro"; +const title = "Healthcare Provider Compliance — NPI, NPPES & Medicare Filings"; +const description = "We handle the federal paperwork that keeps providers billable: Medicare revalidation (PECOS), NPI reactivation, NPPES updates, enrollment, and OIG/SAM exclusion screening. HIPAA not included."; +--- + + +
+
+

Healthcare Provider Compliance

+

+ Your NPI is public. Your Medicare enrollment has a clock on it. We + handle the federal filings that keep you billable so a missed + deadline never deactivates your privileges. +

+
+ +
+

The obligations we cover

+ +
+ +
+

Not sure where you stand?

+

+ Run our free NPI compliance check. Enter your NPI and we'll tell you + whether your record is active, whether your Medicare revalidation is due + or past due, and whether anything in the public registries needs attention. +

+

+ Run the free NPI compliance check → +

+
+ +
+

+ Note: We handle federal registry and enrollment + compliance. We do not provide HIPAA privacy/security program + services. That's a separate specialty. +

+
+ +
+ Check your revalidation status → +
+
+ + + diff --git a/site/src/pages/services/healthcare/medicare-enrollment.astro b/site/src/pages/services/healthcare/medicare-enrollment.astro new file mode 100644 index 0000000..2770c87 --- /dev/null +++ b/site/src/pages/services/healthcare/medicare-enrollment.astro @@ -0,0 +1,69 @@ +--- +import Base from "../../../layouts/Base.astro"; +const title = "Medicare Enrollment (PECOS) — Get Enrolled and Billable"; +const description = "New to Medicare or adding a practice location? We assemble and file your CMS-855 enrollment in PECOS: taxonomy, practice location, authorized official, EFT, and reassignments."; +--- + + +
+
+

Medicare Enrollment (PECOS)

+

+ Before you can bill Medicare, you have to be enrolled in PECOS with a + clean CMS-855. We build the package, file it, and shepherd it through + your MAC to approval. +

+
+ +
+

Who needs to enroll

+
    +
  • New providers just out of training or newly licensed and ready to bill Medicare.
  • +
  • Providers joining a group who need a reassignment (855R) to the group's billing.
  • +
  • Groups and suppliers establishing a new billing entity (855B).
  • +
  • Providers adding a practice location or changing how they bill.
  • +
+
+ +
+

What we file

+
    +
  • CMS-855I — individual physician/non-physician practitioner enrollment.
  • +
  • CMS-855B — group practice / supplier enrollment.
  • +
  • CMS-855R — reassignment of benefits to a group.
  • +
  • CMS-588 (EFT) — electronic funds transfer setup so you get paid.
  • +
  • CMS-460 — participation agreement, if you're enrolling as participating.
  • +
+
+ We confirm your taxonomy code, practice + location, and authorized official match + across NPPES and PECOS before we file, because a mismatch is the #1 + cause of enrollment rejections. +
+
+ +
+

How it works

+
    +
  1. You place the order and give us your NPI and basic practice details.
  2. +
  3. We pull your NPPES record and identify exactly which 855 forms apply.
  4. +
  5. We assemble the package and send it to you for signature/attestation.
  6. +
  7. We submit in PECOS and track it through your MAC, resolving any development requests.
  8. +
+
+ +
+ Start my Medicare enrollment — $499 → +
+
+ + + diff --git a/site/src/pages/services/healthcare/npi-revalidation.astro b/site/src/pages/services/healthcare/npi-revalidation.astro new file mode 100644 index 0000000..9ea80b0 --- /dev/null +++ b/site/src/pages/services/healthcare/npi-revalidation.astro @@ -0,0 +1,84 @@ +--- +import Base from "../../../layouts/Base.astro"; +const title = "Medicare Revalidation Filing (PECOS) — Don't Let Your Billing Privileges Lapse"; +const description = "CMS requires every enrolled provider and supplier to revalidate their Medicare enrollment every 5 years. Miss your deadline and your billing privileges are deactivated. We prepare and file your revalidation in PECOS."; +--- + + +
+
+

Medicare Revalidation Filing

+

+ Every Medicare-enrolled provider has to revalidate every 5 years. If + you miss your due date, CMS deactivates your billing privileges and + every claim after that bounces. We file your revalidation for you. +

+
+ +
+

What revalidation is

+

+ Under 42 CFR § 424.515, CMS re-verifies the + information of every enrolled provider and supplier on a recurring + cycle (every 5 years for most providers, every 3 years for DMEPOS + suppliers). You revalidate your entire enrollment record in + PECOS (the Provider Enrollment, Chain, and Ownership + System) using the CMS-855 family of forms. +

+
+ The deadline is real. CMS publishes a revalidation due + date for every provider. Submit late or not at all and your Medicare + billing privileges are deactivated. Reactivating means a gap + in payments and a fresh round of paperwork. +
+
+ +
+

How do I know if I'm due?

+

+ CMS posts a due date for every enrolled provider. Many providers are + already past due and don't know it because the + notification letter went to an old address. Run our free check and + we'll look up your status: +

+

+ Check my revalidation status (free) → +

+
+ +
+

What we do

+
    +
  • Pull your current PECOS enrollment record and confirm what CMS has on file.
  • +
  • Reconcile your practice locations, reassignments, ownership, and authorized officials.
  • +
  • Complete the correct CMS-855 (855I for individuals, 855B for groups/suppliers, 855R for reassignments).
  • +
  • Submit the revalidation in PECOS and track it through to approval.
  • +
  • Flag and resolve any development requests from your MAC before they cause a deactivation.
  • +
+
+ +
+

Why not do it yourself

+
    +
  • PECOS is unforgiving. One wrong reassignment or a stale practice location triggers a development request, and the clock keeps ticking.
  • +
  • The forms branch. Whether you file 855I, 855B, 855R, or a combination depends on how you bill. Pick wrong and you start over.
  • +
  • Deactivation is retroactive. If you miss the date, the gap isn't forgiven; claims in the gap stay denied.
  • +
+
+ +
+ File my Medicare revalidation — $399 → +
+
+ + + diff --git a/site/src/pages/tools/npi-compliance-check.astro b/site/src/pages/tools/npi-compliance-check.astro new file mode 100644 index 0000000..87351d7 --- /dev/null +++ b/site/src/pages/tools/npi-compliance-check.astro @@ -0,0 +1,327 @@ +--- +import Base from "../../layouts/Base.astro"; +--- + + +
+ + + +

NPI Compliance Check

+

+ Enter your provider name or 10-digit NPI to instantly check your NPPES status, + Medicare revalidation due date, OIG exclusion screening, and record currency. +

+ +
+ This tool queries publicly available NPPES, CMS, and OIG data for informational + purposes only. Always confirm critical compliance matters directly with CMS. + This is not legal advice. +
+ + +
+ + +
+ + + +
+ + +
+
+ or enter NPI directly +
+
+ + + +
+ + +
+

+ Don't know your NPI? + Look it up in NPPES +

+
+ + + + + + + + + +
+ + + diff --git a/site/src/partials/nav.html b/site/src/partials/nav.html index 4692453..368f7ff 100644 --- a/site/src/partials/nav.html +++ b/site/src/partials/nav.html @@ -1,7 +1,7 @@ \ No newline at end of file + \ No newline at end of file