# NPI Registry Scraper & API - US Doctors, Dentists & Clinics (`neverempty/npi-registry-scraper`) Actor

For medical device and pharma sales, credentialing and provider directories: search the official NPPES registry for US doctors, dentists and clinics by specialty, city, ZIP, surname or organisation. 1,200 unique providers per run with zero duplicate rows. No API key, no login.

- **URL**: https://apify.com/neverempty/npi-registry-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Lead generation, Developer tools, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.84 / 1,000 provider returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## NPI Registry Scraper & API

**For healthcare sales and lead lists, provider-directory and credentialing work, claims and payer analytics, and medical market research**: US healthcare providers — doctors, dentists, therapists, pharmacies, clinics and hospitals — from the official NPPES NPI registry run by the Centers for Medicare & Medicaid Services, as flat rows.

**The registry's paging silently hands you the same records over and over, and this Actor refuses to.** Measured against the live API on 2026-09-10: `limit` is capped at 200 (ask for 1,200 and you get 200, no error), and **the paging offset stops being honoured past roughly 1,000 records** — `skip=1000` and `skip=20000` overlap **200 of 200**. A reader that trusts the offset keeps collecting the same page forever. Asking this Actor for 2,000 dentists in California returns **1,200 unique providers and zero duplicates**, not 2,000 rows of which 800 are repeats — and it tells you the ceiling was hit and suggests how to narrow the search.

**The registry also returns its errors with HTTP 200.** A rejected search comes back as a normal-looking success with an error body, so a typo looks exactly like "no such providers". Here you get the registry's exact wording back instead. Measured on 2026-09-10 for New York, `Cardiovascular Disease` returned **200** providers and `Kaiser Permanente` returned **200**, while a rejected term was reported as rejected.

Organisations and individuals are separated by `providerType`, so a table of 500 providers splits cleanly. **You are charged only for unique rows you receive.** No API key, no scraping, no proxy. Export as JSON, CSV or Excel.

```json
{
  "specialty": "dentist",
  "state": "CA",
  "maxProviders": 500
}
```

### What this fixes about the raw API

Everything below was measured against the live API on 2026-09-10, not read from documentation.

#### 1. The registry returns its errors with HTTP 200

A rejected search does not come back as an error status. It comes back as **HTTP 200** with this body:

```json
{"Errors":[{"description":"Field state requires additional search criteria","field":"state","number":"07"}]}
```

There is no `result_count` at all. Read the status and count the results and you conclude the search worked and
found nobody - so `state=CA` on its own silently becomes "there are no providers in California".

Three rejections measured:

| What was asked | What the registry says |
|---|---|
| Nothing at all | `No valid search criteria provided` |
| `state=CA` alone | `Field state requires additional search criteria` |
| An unknown specialty | `No taxonomy codes found with entered description` |

Each comes back here as a row that says the search was **rejected**, with the registry's own wording. A search
that genuinely matched nobody gets a different row that says so. Neither is charged, and neither is ever
reported as the other.

#### 2. `limit` is capped at 200, and paging stops working past about 1,000

Ask for 201 and you get 200. Ask for 1,200 and you get 200. No error, no warning.

Worse, the paging offset stops being honoured past roughly 1,000 records. Measured on
`taxonomy_description=dentist&state=CA&limit=200`:

| `skip` | first NPI returned | last NPI returned |
|---|---|---|
| 800 | 1932772191 | 1295652956 |
| 1,000 | 1235460916 | 1710697255 |
| 1,200 | 1235460916 | 1710697255 |
| 2,000 | 1235460916 | 1710697255 |
| 20,000 | 1235460916 | 1710697255 |

`skip=1000` and `skip=20000` overlap **200 of 200**. A reader that trusts the offset keeps collecting the same
page forever. This Actor remembers every NPI it has delivered, drops repeats before they reach you, and stops
when a page brings nothing new - then returns an uncharged row saying how far the search could actually reach
and suggesting how to narrow it. Asking for 2,000 dentists in California returns **1,200 unique providers and
zero duplicates**, not 2,000 rows of which 800 are repeats.

#### 3. A state on its own is not a search

The registry needs a city, a postal code, a specialty, a surname or an organisation name alongside it. That is
checked before the run starts, with a message saying what to add, rather than burning a request to be told no.

#### 4. Individuals and organisations are different records

An individual (NPI-1) has a surname, a credential and a licence. An organisation (NPI-2) has a legal name and
an authorised official. Fields belonging to the other kind are left empty rather than filled with a blank
string, so a table of 500 providers can be split cleanly by `providerType`.

#### 5. Personal sex is not returned

The registry publishes a `sex` field on individual records. It is not in the output here: it is a personal
attribute with no business use in a provider directory, and leaving it out costs the dataset nothing.

#### 6. The registry's specialty vocabulary is not everyday English

`taxonomy_description` matches the registry's own official taxonomy names, not the words people use. Measured
on 2026-09-10 for New York:

| What you type | What you get |
|---|---|
| `cardiologist` | **Rejected** - `No taxonomy codes found with entered description` |
| `cardiology` | 7 providers |
| `Cardiovascular Disease` | 200 providers |

The same applies to organisation names, which match the registered legal name: `Kaiser` returned 15 records
while `Kaiser Permanente` returned 200. When a term is rejected, you get the registry's exact wording back
rather than an empty table, so you can see that the word was the problem.

### What you get

`npi`, `npiUrl`, `providerType`, `organizationName`, `lastName`, `firstName`, `middleName`, `credential`,
`soleProprietor`, `authorizedOfficialName`, `authorizedOfficialTitle`, `primaryTaxonomy`,
`primaryTaxonomyCode`, `primaryTaxonomyLicense`, `primaryTaxonomyState`, `allTaxonomies`, `practiceAddress1`,
`practiceCity`, `practiceState`, `practicePostalCode`, `practicePhone`, `practiceFax`, `mailingAddress1`,
`mailingCity`, `mailingState`, `mailingPostalCode`, `otherNames`, `enumerationDate`, `lastUpdated`,
`deactivationDate`, `providerStatus`.

The practice address and the mailing address are separate columns, because for a large share of providers they
are different places and collapsing them loses the one you wanted.

### Input

| Field | What it does |
|---|---|
| `specialty` | A specialty as the registry writes it: `dentist`, `cardiology`, `pharmacy`. |
| `state` | Two-letter code. Refused on its own - combine it with another field. |
| `city` / `postalCode` | Either works as a search on its own. |
| `lastName` / `firstName` | Individual practitioners. |
| `organizationName` | Practices, clinics, hospitals, suppliers. |
| `npi` | A specific 10-digit NPI. |
| `providerType` | `NPI-1` individual or `NPI-2` organisation. Refused on its own. |
| `countryCode` | Two-letter code. Refused on its own. |
| `maxProviders` | How many to return. Collected 200 at a time, and capped in practice by the registry's ~1,000-record paging ceiling for any one search. You are charged only for unique rows you receive. |
| `maxRetries` | Attempts per request. |

No search field carries a default, so nothing is ever added to your search that you did not ask for. If you run
it with everything blank it falls back to dentists in California and says so in the log.

### Source and licence

NPPES NPI Registry API (`npiregistry.cms.hhs.gov`). The registry is published by the US Centers for Medicare &
Medicaid Services as public data; works of the US federal government are in the public domain. The records are
provider **business** information that CMS publishes for exactly this purpose. This Actor makes plain HTTPS
requests to the documented JSON endpoint; it does not scrape any web page and needs no API key or proxy.

# Actor input Schema

## `specialty` (type: `string`):

A specialty as the registry writes it: dentist, cardiology, physical therapist, pharmacy, nurse practitioner. A description the registry does not know is rejected with its own message rather than returning everybody.

## `state` (type: `string`):

Two-letter state code such as CA or NY. On its own this is refused by the registry - it has to be combined with a city, a postal code, a specialty, a surname or an organisation name.

## `city` (type: `string`):

City name, such as Los Angeles or Boston. Matched against the address the provider registered.

## `postalCode` (type: `string`):

A ZIP code such as 90210. Works on its own without a state.

## `lastName` (type: `string`):

Surname of an individual provider. Individual records carry the surname, credential and licence; organisation records carry the legal name and the authorised official instead.

## `firstName` (type: `string`):

First name of an individual provider.

## `organizationName` (type: `string`):

Name of a practice, clinic, hospital or supplier, such as Kaiser.

## `npi` (type: `string`):

A specific 10-digit NPI to look up. An NPI that is not in the registry comes back as a search that found nothing, which is a different row from a search the registry rejected.

## `providerType` (type: `string`):

NPI-1 = individual practitioner, NPI-2 = organisation. On its own this is refused by the registry and has to be combined with another field.

## `countryCode` (type: `string`):

Two-letter country code, US by default in the registry. On its own this is refused and has to be combined with another field.

## `maxProviders` (type: `integer`):

How many providers to return. You are charged for the rows you actually receive, and a provider is never delivered or charged twice. The registry returns at most 200 per request whatever you ask for, and it stops honouring the paging offset past roughly 1,000 records for one search - beyond that it repeats the same page. This Actor stops there and tells you, so a large number here does not turn into a large bill for repeats. Narrow the search (a city, a postal code, a different specialty) to reach more.

## `maxRetries` (type: `integer`):

How many times to try a request in total when the registry does not answer.

## Actor input object example

```json
{
  "specialty": "dentist",
  "state": "CA",
  "providerType": "",
  "maxProviders": 200,
  "maxRetries": 4
}
```

# Actor output Schema

## `results` (type: `string`):

One row per healthcare provider from the NPI registry, plus one row when the registry rejected the search, matched nobody, or could not be read.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "specialty": "dentist",
    "state": "CA"
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/npi-registry-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "specialty": "dentist",
    "state": "CA",
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/npi-registry-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "specialty": "dentist",
  "state": "CA"
}' |
apify call neverempty/npi-registry-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/npi-registry-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/tQdh7qyHRCQeuHVqe/builds/IdDseTTjIpqfOz2Eb/openapi.json
