NPI Lookup in 5 Minutes: Search 9 Million Providers From Your App
Every healthcare app needs provider data. Here's how to search, look up, and filter the NPI registry from TypeScript — with code you can ship today.

Every healthcare app eventually needs to answer the same question: who is this provider? Whether you're verifying a referral, populating a directory, or matching claims to clinicians, you need NPI data. The National Provider Identifier registry has 9 million+ records — and querying it directly from CMS is an exercise in patience and XML parsing.
This tutorial shows how to search, look up, and filter NPI data using FHIRfly's API and TypeScript SDK. Working code throughout, no CMS SOAP endpoints required.
What's in an NPI Record?
Every healthcare provider in the US — individual clinicians and organizations — gets a unique 10-digit NPI from CMS. The NPI record includes:
- Identity: Name, credentials, entity type (individual vs. organization)
- Taxonomies: Specialty classifications from the NUCC taxonomy (e.g.,
207X00000X = Orthopedic Surgery)
- Addresses: Practice location, mailing address, secondary locations
- Status: Active/deactivated, enumeration date, last update
FHIRfly enriches the raw NPPES data with NUCC taxonomy display names and updates daily.
Setup
npm install @fhirfly-io/terminology
import { Fhirfly } from "@fhirfly-io/terminology";
const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });
Look Up a Single Provider
If you have an NPI, pull the full record:
const result = await fhirfly.npi.lookup("1750638235", {
shape: "standard",
include: ["display"],
});
console.log(result.data.display.full_name);
// "John R. Smith, MD"
console.log(result.data.taxonomies[0].classification);
// "Orthopedic Surgery"
console.log(result.data.practice_address);
// { line1: "123 Medical Center Dr", city: "Denver", state: "CO", postal: "80202" }
Three response shapes control how much data you get back:
| Shape | Fields | Best for |
|---|
compact | Name, specialty, location, active status | Search results, lists |
standard | + Structured name, taxonomies, address, dates | Detail views, verification |
full | + Secondary locations, mailing address, deactivation history, identifiers | Compliance, deep analysis |
The include: ["display"] option adds pre-formatted strings — useful when you need display-ready text without assembling it yourself.
Search Providers
The search endpoint supports full-text queries with filters:
const results = await fhirfly.npi.search(
{ q: "cardiology", state: "CA", entity_type: "individual" },
{ limit: 10 }
);
console.log(`Found ${results.total} cardiologists in California`);
for (const provider of results.items) {
console.log(`${provider.name} — ${provider.specialty} — ${provider.location}`);
}
// "Maria Garcia, MD — Cardiovascular Disease — Los Angeles, CA"
// "James Chen, DO — Interventional Cardiology — San Francisco, CA"
// ...
Search returns the compact shape by default — enough for list views without over-fetching.
Available Filters
Combine text search with any of these:
// Search by name and location
await fhirfly.npi.search({ name: "smith", state: "TX", city: "Houston" });
// Find organizations by specialty
await fhirfly.npi.search({
organization: "medical center",
state: "NY",
entity_type: "organization",
});
// Exact taxonomy code match
await fhirfly.npi.search({ q: "physical therapy", taxonomy: "225100000X" });
// Filter by ZIP prefix
await fhirfly.npi.search({ specialty: "dermatology", postal_code: "90210" });
Every search requires at least one text parameter (q, name, organization, or specialty). Filter-only queries return a 400 — this is intentional to prevent unbounded scans across 9 million records.
Pagination
let page = 1;
let hasMore = true;
while (hasMore) {
const results = await fhirfly.npi.search(
{ specialty: "family medicine", state: "CO" },
{ limit: 50, page }
);
for (const provider of results.items) {
await processProvider(provider);
}
hasMore = results.has_more;
page++;
}
Pagination caps at 10,000 results (100 pages × 100 per page). The total_capped flag tells you if the real count exceeds this — tighten your filters if it does.
Facets
Search responses include facet counts for entity type and state, useful for building filter UIs:
const results = await fhirfly.npi.search({ q: "cardiology" });
console.log(results.facets.entity_type);
// { individual: 45200, organization: 3100 }
console.log(results.facets.state);
// { CA: 5200, NY: 4800, TX: 4100, FL: 3900, ... }
Batch Lookups
When you have a list of NPIs — from a claims file, a referral list, an enrollment spreadsheet — use the batch endpoint instead of N individual calls:
const response = await fhirfly.npi.lookupMany(
["1750638235", "1447440417", "0000000000"],
{ shape: "standard" }
);
for (const result of response.results) {
if (result.status === "ok") {
console.log(`${result.npi}: ${result.data.display?.full_name}`);
} else {
console.log(`${result.npi}: ${result.status} — ${result.error}`);
}
}
// "1750638235: John R. Smith, MD"
// "1447440417: Valley Medical Group"
// "0000000000: not_found — NPI not found in NPPES"
Batch accepts up to 100 NPIs per request. Each result includes its own status, so partial failures don't break the whole call.
REST API (Without the SDK)
The SDK wraps a straightforward REST API. If you're working in Python, Go, or another language:
# Single lookup
curl -H "x-api-key: $FHIRFLY_API_KEY" \
"https://api.fhirfly.io/v1/npi/1750638235?shape=standard&include=display"
# Search
curl -H "x-api-key: $FHIRFLY_API_KEY" \
"https://api.fhirfly.io/v1/npi/search?q=cardiology&state=CA&limit=10"
# Batch
curl -X POST -H "x-api-key: $FHIRFLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"codes": ["1750638235", "1447440417"]}' \
"https://api.fhirfly.io/v1/npi/_batch?shape=standard"
Data Freshness
FHIRfly checks for NPPES updates daily and applies them as soon as CMS publishes new data — typically weekly. Every response includes provenance metadata so you know exactly what you're working with:
const result = await fhirfly.npi.lookup("1750638235", { shape: "full" });
console.log(result.meta.source.source_update_cadence);
// "weekly"
console.log(result.meta.source.fhirfly_updated_at);
// "2026-03-13T06:00:00.000Z"
console.log(result.meta.legal.source_name);
// "CMS NPPES with NUCC Taxonomy"
NPI data is public domain — no licensing restrictions on what you build with it.
Key Takeaways
- Three shapes (
compact, standard, full) let you fetch only the data you need
- Search requires a text query plus optional filters — no unbounded scans
- Batch endpoint handles up to 100 NPIs per request with per-item status
- Daily sync from CMS NPPES with provenance metadata on every response
- Public domain data — no licensing restrictions
Next Steps
Grab an API key from the FHIRfly dashboard and try a search. The NPI API docs cover every parameter in detail, and the @fhirfly-io/terminology SDK handles auth, retries, and types out of the box.