The Mine Works
Browse on Apify
NPI Registry API: How to Look Up Any US Healthcare Provider Programmatically
← All posts
tutorial June 22, 2026 · 7 min read Updated June 22, 2026

NPI Registry API: How to Look Up Any US Healthcare Provider Programmatically

CMS publishes the National Provider Identifier registry as a free API. Here is how to search by provider name, specialty, location, and NPI number — and what the data contains.

Try the scraper

The actor referenced in this article is live on Apify. Pay only for results delivered.

Open on Apify →

Every US healthcare provider who bills insurance has a National Provider Identifier. It is a 10-digit number assigned by CMS, required for Medicare and Medicaid billing, and used as the universal identifier across payers, networks, and health records systems.

CMS publishes the full NPI registry as a free, public API. No authentication is required. You can search by name, specialty, organization, and geography, and the response contains the provider’s address, credential, taxonomy codes, and enumeration date.

This post covers what NPI numbers are, the difference between Type 1 and Type 2 NPIs, the NPPES API query structure, and Python code for searching providers by specialty and location, bulk downloading all active providers in a state, and validating provider lists.

TL;DR: The NPPES NPI Registry API is at https://npiregistry.cms.hhs.gov/api/. No auth required. Returns JSON with provider demographics, addresses, and taxonomy codes. Pagination via number (limit) and skip parameters. The full registry is also available as a bulk CSV download at CMS, updated monthly.

What NPI Numbers Are

The National Plan and Provider Enumeration System (NPPES) was established in 2004. Every provider who transmits health information electronically in HIPAA-covered transactions must have an NPI.

There are two types:

Type 1 NPI (Individual). Assigned to individual human healthcare providers: physicians, nurses, dentists, therapists, chiropractors, and others. Each individual gets exactly one Type 1 NPI for life, regardless of where they practice or how many organizations they work for.

Type 2 NPI (Organization). Assigned to organizations: hospitals, clinics, group practices, pharmacies, labs. An organization can have multiple Type 2 NPIs if it operates in multiple locations or has distinct subparts (a hospital with a separate skilled nursing facility, for example).

The NPI itself does not tell you specialty. Specialty comes from the provider’s declared taxonomy codes, which follow the Healthcare Provider Taxonomy Code Set maintained by NUCC.

The NPPES API

Base URL: https://npiregistry.cms.hhs.gov/api/

Method: GET, JSON response. No API key required.

Key query parameters:

ParameterDescriptionExample
numberNPI number (exact match)number=1234567890
enumeration_typeNPI-1 (individual) or NPI-2 (org)enumeration_type=NPI-1
first_nameProvider first name (partial match)first_name=James
last_nameProvider last name (partial match)last_name=Smith
organization_nameOrganization name (partial match)organization_name=Mount+Sinai
taxonomy_descriptionSpecialty description (partial match)taxonomy_description=Cardiology
cityCitycity=Chicago
state2-letter state codestate=IL
postal_codeZIP code (5 or 9 digit)postal_code=60601
country_code2-letter country codecountry_code=US
limitResults per page (max 200)limit=200
skipPagination offsetskip=400
versionAPI versionversion=2.1

Always include version=2.1. Older versions return a different response structure.

Response Structure

A typical response looks like this:

{
  "result_count": 1,
  "results": [
    {
      "created_epoch": "1197835200000",
      "enumeration_type": "NPI-1",
      "last_updated_epoch": "1623888000000",
      "number": "1234567890",
      "addresses": [
        {
          "country_code": "US",
          "country_name": "United States",
          "address_purpose": "LOCATION",
          "address_type": "DOM",
          "address_1": "123 Medical Dr",
          "city": "Chicago",
          "state": "IL",
          "postal_code": "606015678",
          "telephone_number": "312-555-0100",
          "fax_number": "312-555-0101"
        },
        {
          "address_purpose": "MAILING",
          "address_1": "PO Box 1000",
          "city": "Chicago",
          "state": "IL"
        }
      ],
      "taxonomies": [
        {
          "code": "207RC0000X",
          "desc": "Cardiovascular Disease",
          "primary": true,
          "state": "IL",
          "license": "036-075890"
        }
      ],
      "basic": {
        "first_name": "James",
        "last_name": "Smith",
        "credential": "MD",
        "gender": "M",
        "enumeration_date": "2007-12-17",
        "last_updated": "2021-06-17",
        "status": "A",
        "name_prefix": "DR."
      }
    }
  ]
}

Key fields to extract:

  • number: the NPI itself
  • enumeration_type: NPI-1 or NPI-2
  • basic.credential: MD, DO, NP, RN, etc.
  • basic.status: A (active) or D (deactivated)
  • taxonomies: list of specialty codes; the one with primary: true is the declared primary specialty
  • addresses: separate LOCATION and MAILING addresses; address_purpose = "LOCATION" is the practice location

Python: Searching All Cardiologists in Chicago

import requests
import pandas as pd
import time

NPPES_BASE = "https://npiregistry.cms.hhs.gov/api/"

def search_nppes(params, max_results=1000):
    """
    Search the NPPES registry with pagination.
    Returns a list of provider dicts.
    """
    base_params = {"version": "2.1", "limit": 200}
    base_params.update(params)

    all_results = []
    skip = 0

    while len(all_results) < max_results:
        base_params["skip"] = skip
        response = requests.get(NPPES_BASE, params=base_params)
        response.raise_for_status()
        data = response.json()

        results = data.get("results", [])
        if not results:
            break

        all_results.extend(results)
        result_count = data.get("result_count", 0)

        print(f"Fetched {len(all_results)} / {min(result_count, max_results)}")

        if len(all_results) >= result_count:
            break

        skip += 200
        time.sleep(0.2)

    return all_results[:max_results]

def flatten_provider(p):
    """Flatten a provider record to a single dict for dataframe use."""
    basic = p.get("basic", {})

    # Get practice location address
    location_addr = next(
        (a for a in p.get("addresses", []) if a.get("address_purpose") == "LOCATION"),
        {}
    )

    # Get primary taxonomy
    primary_tax = next(
        (t for t in p.get("taxonomies", []) if t.get("primary")),
        p.get("taxonomies", [{}])[0] if p.get("taxonomies") else {}
    )

    return {
        "npi":            p.get("number"),
        "type":           p.get("enumeration_type"),
        "status":         basic.get("status"),
        "first_name":     basic.get("first_name", ""),
        "last_name":      basic.get("last_name", ""),
        "org_name":       basic.get("organization_name", ""),
        "credential":     basic.get("credential", ""),
        "gender":         basic.get("gender", ""),
        "enumerated":     basic.get("enumeration_date", ""),
        "last_updated":   basic.get("last_updated", ""),
        "specialty_code": primary_tax.get("code", ""),
        "specialty_desc": primary_tax.get("desc", ""),
        "address_1":      location_addr.get("address_1", ""),
        "city":           location_addr.get("city", ""),
        "state":          location_addr.get("state", ""),
        "zip":            location_addr.get("postal_code", ""),
        "phone":          location_addr.get("telephone_number", ""),
    }

# Search: all individual cardiologists in Chicago
chicago_cardiologists = search_nppes({
    "enumeration_type":    "NPI-1",
    "taxonomy_description": "Cardiology",
    "city":                "Chicago",
    "state":               "IL",
    "country_code":        "US",
}, max_results=500)

df = pd.DataFrame([flatten_provider(p) for p in chicago_cardiologists])
df = df[df["status"] == "A"]  # active providers only
print(f"Active cardiologists in Chicago: {len(df)}")
print(df[["npi", "first_name", "last_name", "credential", "address_1", "zip"]].head(10).to_string(index=False))

Python: Bulk Downloading All Active NPIs in a State

def bulk_state_download(state_code, enumeration_type="NPI-1", max_results=50000):
    """
    Download all active providers in a state.
    Note: NPPES API caps results at ~1200 per query due to pagination limits.
    For complete state datasets, use the monthly bulk CSV download from CMS.
    This function is appropriate for subsets (by specialty + state).
    """
    return search_nppes({
        "enumeration_type": enumeration_type,
        "state":            state_code,
        "country_code":     "US",
    }, max_results=max_results)

# All NPI-2 organizations in Texas
tx_orgs = bulk_state_download("TX", enumeration_type="NPI-2", max_results=5000)
tx_df = pd.DataFrame([flatten_provider(p) for p in tx_orgs])
print(f"Organizations in Texas: {len(tx_df)}")

For truly complete state downloads, the NPPES API hits practical limits around 1,200 results for broad queries. For a full state-level dataset, download the monthly bulk file from https://download.cms.gov/nppes/NPI_Files.html. The monthly full replacement file is a CSV with all active NPIs and is about 8GB uncompressed.

Python: Validating a Provider List Against the Registry

def validate_provider_list(npi_list):
    """
    Validate a list of NPI numbers against the live registry.
    Returns a dict mapping each NPI to its status and key fields.
    """
    results = {}
    for npi in npi_list:
        response = requests.get(NPPES_BASE, params={"version": "2.1", "number": npi})
        data = response.json()
        providers = data.get("results", [])
        if not providers:
            results[npi] = {"found": False, "status": None}
        else:
            p = providers[0]
            results[npi] = {
                "found":      True,
                "status":     p.get("basic", {}).get("status"),
                "name":       f"{p.get('basic', {}).get('first_name', '')} {p.get('basic', {}).get('last_name', '')}".strip() or p.get("basic", {}).get("organization_name", ""),
                "credential": p.get("basic", {}).get("credential", ""),
                "type":       p.get("enumeration_type"),
            }
        time.sleep(0.1)

    return results

# Check a list of NPIs from a billing spreadsheet
npis_to_check = ["1234567890", "0987654321", "1111111112"]
validation = validate_provider_list(npis_to_check)
for npi, info in validation.items():
    status = info.get("status")
    print(f"NPI {npi}: found={info['found']}, status={status}, name={info.get('name','')}")

Status A is active. Status D is deactivated. Claims submitted with a deactivated NPI will be rejected by payers.

Use Cases

Healthcare market research. Count providers by specialty and geography to identify underserved markets. Compare specialist density across ZIP codes or counties for healthcare access analysis.

Provider directory building. Pull all NPIs for a specialty within a defined geography and enrich with the taxonomy, address, and credential data from NPPES. This gives you a starter directory without manual data collection.

Billing validation. Before submitting claims or processing provider records, validate each NPI against the live registry. Flag deactivated NPIs, mismatched names, and wrong provider types before they cause claim rejections.

Referral network analysis. In conjunction with Medicare claims data (also public), NPI numbers let you map referral relationships between physicians. The NPI is the join key between NPPES demographic data and claims-level utilization data.

The NPI Registry healthcare scraper handles paginated bulk pulls, scheduled monthly refreshes, and output normalization for teams building provider databases or compliance tools on top of NPPES data.

Related Actor

Try the scraper referenced in this article — live on Apify, pay only for results.

Open npi-registry-healthcare on Apify →