# West Virginia Physician License Roster Scraper (`automation-lab/west-virginia-physician-license-roster`) Actor

Download official West Virginia active MD, physician assistant, and podiatric physician rosters with credential numbers, expiration dates, status context, and provenance.

- **URL**: https://apify.com/automation-lab/west-virginia-physician-license-roster.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.44 / 1,000 item extracteds

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

## West Virginia Physician License Roster Scraper

Download the official **West Virginia physician license roster** and turn its monthly spreadsheets into clean JSON records.

The Actor reads the West Virginia Board of Medicine's current active-status rosters for:

- medical doctors (MDs),
- physician assistants (PAs), and
- podiatric physicians (DPMs).

Each result includes practitioner identity, profession, credential number, expiration date, active-roster context, official source URL, roster publication date, and retrieval timestamp.

### What does this Actor do?

The Board publishes separate Excel workbooks and multiple credential worksheets.
This Actor discovers the current official download links, downloads the selected workbooks, and normalizes the rows into one consistent dataset.

You can:

- export a complete profession roster;
- find a practitioner by name;
- look up a credential number;
- select standard or nonstandard credentials;
- limit the dataset for quick checks;
- schedule recurring Apify runs for monthly compliance refreshes;
- send results to spreadsheets, databases, webhooks, or data pipelines.

The Actor does not scrape third-party directories.
Every record links back to the official Board roster.

### Who is it for?

#### Healthcare credentialing teams

Refresh active-status source data before onboarding or periodic re-verification.

#### Compliance teams

Build repeatable snapshots with publication and retrieval dates for audit trails.

#### Hospitals and staffing companies

Filter official rosters by profession and compare exported datasets between runs.

#### Researchers and analysts

Convert state-published XLSX files into API-friendly JSON, CSV, or Excel exports.

#### Developers

Use a stable normalized schema instead of maintaining workbook and worksheet parsing code.

### Why use this West Virginia physician license roster scraper?

- **Official source** — data comes from `wvbom.wv.gov`.
- **Three professions** — MD, PA, and DPM rosters use the same output shape.
- **Multiple credential types** — standard licenses, special licenses, permits, and telehealth registrations are supported when published.
- **Current links** — workbook URLs are discovered from the roster page on each run.
- **Normalized dates** — expiration and roster dates use `YYYY-MM-DD`.
- **Provenance included** — source URLs, worksheet context, and retrieval time remain attached to every record.
- **No browser required** — the Actor uses the Board's public roster downloads directly.

### What data can I extract?

| Field | Meaning |
|---|---|
| `fullName` | Combined practitioner name |
| `firstName` | First name from the Board workbook |
| `middleName` | Middle name or initial, when present |
| `lastName` | Last name from the Board workbook |
| `suffix` | Name suffix, when present |
| `profession` | Normalized profession name |
| `professionCode` | `MD`, `PA`, or `DPM` |
| `credentialType` | Normalized license, permit, or registration type |
| `licenseNumber` | Published credential number |
| `expirationDate` | Credential expiration date in `YYYY-MM-DD` |
| `rosterStatus` | `Active`, reflecting the source roster scope |
| `rosterPublishedDate` | Date shown on the Board roster page |
| `rosterContext` | Original worksheet status heading |
| `worksheet` | Original workbook worksheet name |
| `notes` | Board-published notes, when available |
| `sourceUrl` | Official workbook download URL |
| `rosterPageUrl` | Official roster landing page |
| `retrievedAt` | UTC retrieval timestamp |

### Supported credential types

The `credentialTypes` input accepts:

| Value | Source worksheet scope |
|---|---|
| `license` | Standard active licenses |
| `special-license` | Special active licenses |
| `temporary-license` | Temporary active licenses |
| `educational-permit` | Educational permits |
| `reciprocal-educational-permit` | Reciprocal educational permits |
| `interstate-telehealth-registration` | Interstate telehealth registrations |

Not every profession has every worksheet.
Selecting an unavailable combination simply yields no rows from that combination.

### How to run the Actor

1. Open the Actor in Apify Console.
2. Choose one or more professions.
3. Choose one or more credential types.
4. Optionally enter a practitioner name or credential number.
5. Set the maximum number of records.
6. Click **Start**.
7. Open the **Dataset** tab to inspect or export results.

For a complete standard-license refresh, leave all three professions selected, choose `license`, and raise `maxItems` to the required volume.

### Input parameters

| Parameter | Type | Default | Description |
|---|---|---:|---|
| `professions` | string array | MD, PA, DPM | Profession workbooks to download |
| `credentialTypes` | string array | license | Worksheet types to include |
| `searchTerm` | string | empty | Case-insensitive name or credential-number filter |
| `maxItems` | integer | 1,000 | Maximum records saved, from 1 to 20,000 |

An empty `searchTerm` includes all matching roster rows.
The search filter is applied after downloading the selected official workbooks.

### Input examples

#### Find a doctor by surname

```json
{
  "professions": ["MD"],
  "credentialTypes": ["license"],
  "searchTerm": "Abadir",
  "maxItems": 10
}
```

#### Export active podiatric physicians

```json
{
  "professions": ["DPM"],
  "credentialTypes": ["license"],
  "maxItems": 500
}
```

#### Refresh nonstandard physician credentials

```json
{
  "professions": ["MD", "PA"],
  "credentialTypes": [
    "special-license",
    "educational-permit",
    "interstate-telehealth-registration"
  ],
  "maxItems": 2000
}
```

### Output example

A current standard-license record has this shape:

```json
{
  "fullName": "Sample A Physician",
  "firstName": "Sample",
  "middleName": "A",
  "lastName": "Physician",
  "suffix": null,
  "profession": "Medical Doctor",
  "professionCode": "MD",
  "credentialType": "license",
  "licenseNumber": "12345",
  "expirationDate": "2028-06-30",
  "rosterStatus": "Active",
  "rosterPublishedDate": "2026-08-14",
  "rosterContext": "MEDICAL DOCTORS (MDs) WHO HOLD AN ACTIVE STATUS LICENSE",
  "worksheet": "Licenses",
  "notes": null,
  "sourceUrl": "https://wvbom.wv.gov/download_resource.asp?id=000",
  "rosterPageUrl": "https://wvbom.wv.gov/Rosters.asp",
  "retrievedAt": "2026-08-15T12:00:00.000Z"
}
```

Names and credential numbers in this documentation example are illustrative.
Actual datasets contain the public values in the current Board roster.

### How much does it cost to download West Virginia physician licenses?

Pricing uses one small run-start charge plus a per-record charge.
The exact amount is shown in Apify Console before you start the Actor and varies by your Apify pricing tier.

At the current Bronze tier:

- the run-start charge is **$0.00005**;
- each saved record is **$0.0024**;
- 25 records cost about **$0.06005**;
- 100 records cost about **$0.24005**;
- 1,000 records cost about **$2.40005**.

Only records written to the dataset are charged as items.
Empty workbook rows, filtered-out rows, and failed downloads are not item charges.
Platform compute may be billed separately according to your Apify plan.

### Export and integration options

From the default dataset, export results as:

- JSON,
- JSON Lines,
- CSV,
- Excel,
- XML, or
- RSS.

Common workflows include:

1. schedule a run after the Board's monthly roster update;
2. export the dataset to cloud storage;
3. compare `professionCode + credentialType + licenseNumber` with the prior snapshot;
4. review additions, removals, or changed expiration dates;
5. preserve `rosterPublishedDate` and `retrievedAt` in the audit record.

The Actor reports the source's current active-roster context.
It does not itself send change alerts or retain historical comparisons.
Use Apify schedules, webhooks, integrations, or your own database for those steps.

### Run through the Apify API

Replace `<APIFY_TOKEN>` with your token.

#### cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~west-virginia-physician-license-roster/runs?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"professions":["DPM"],"credentialTypes":["license"],"maxItems":25}'
```

To wait for the run and return dataset items:

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~west-virginia-physician-license-roster/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"professions":["MD"],"credentialTypes":["license"],"searchTerm":"Abadir","maxItems":10}'
```

#### JavaScript

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/west-virginia-physician-license-roster').call({
  professions: ['MD', 'PA', 'DPM'],
  credentialTypes: ['license'],
  maxItems: 1000,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("automation-lab/west-virginia-physician-license-roster").call(
    run_input={
        "professions": ["PA"],
        "credentialTypes": ["license"],
        "maxItems": 100,
    }
)
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(items)
```

### Use with MCP and AI agents

Add Apify MCP to Claude Code:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/west-virginia-physician-license-roster"
```

Use this same JSON configuration in **Claude Desktop**, **Cursor**, or **VS Code**:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/west-virginia-physician-license-roster"
    }
  }
}
```

- **Claude Desktop:** add the `mcpServers` block to the Claude Desktop configuration file.
- **Cursor:** open MCP settings and add the `apify` server with the URL shown above.
- **VS Code:** add the same server definition through your MCP-compatible extension or workspace MCP configuration.

Example prompts:

- “Download the first 100 active West Virginia PA licenses.”
- “Find the active MD roster entry whose name contains Abadir.”
- “Export current DPM license numbers and expiration dates.”
- “Retrieve active interstate telehealth registrations for monthly comparison.”

### Reliability and source behavior

The Actor performs up to three bounded attempts for a failed roster page or workbook download.
It validates that workbook responses are XLSX files before parsing them.
Unrecognized worksheet layouts are skipped with a warning rather than emitted incorrectly.

The source is an official state website and can change without notice.
A run fails when the roster page or a selected profession workbook cannot be downloaded after retries.
This makes missing refreshes visible instead of silently returning misleading data.

### Limits and interpretation

- The source says its roster files are updated monthly, not in real time.
- `Active` describes inclusion in the Board's active-status roster at publication time.
- For physician assistants, the Board notes that an active credential and an activated practice notification may both be required to practice.
- The Actor does not represent a legal certification of current eligibility to practice.
- The Actor does not provide disciplinary history, private contact details, specialty, or practice-location enrichment.
- Empty source workbook rows are ignored.
- Results follow the Board's spelling and name components.

For an individual decision, review the linked official source and applicable Board guidance.

### Troubleshooting

#### Why did I receive fewer records than `maxItems`?

`maxItems` is a ceiling, not a target.
Your profession, credential-type, and search filters may match fewer current rows.
Some selected profession and credential-type combinations do not exist in the published workbooks.

#### Why did a name search return no results?

The search is a literal, case-insensitive substring match against the published full name and credential number.
Try a last name, a shorter spelling, or the credential number without punctuation.

#### Why did the run fail during download?

The official Board website may be temporarily unavailable or may have changed its file links.
Retry later after checking the official roster page.
Persistent failures should be reported with the run ID and input.

#### Can I download every standard license?

Yes.
Select all professions, select `license`, leave `searchTerm` empty, and set `maxItems` high enough for the current roster volume.
The input supports up to 20,000 records.

### Responsible use and legality

The Actor processes public professional-license roster information published by the West Virginia Board of Medicine.
Use it for lawful verification, compliance, research, and data-management purposes.

You are responsible for:

- following applicable laws and contractual obligations;
- avoiding harassment, discrimination, or harmful profiling;
- applying appropriate access controls to exported datasets;
- checking the official source before high-impact decisions;
- respecting corrections and updates published by the regulator.

Public availability does not remove your responsibility to use personal information fairly and securely.

### Related Actors

For federal provider identity and taxonomy enrichment, use [NPPES NPI Registry Provider Search](https://apify.com/automation-lab/npi-registry-provider-search).
It is a separate federal registry and should not be treated as a substitute for West Virginia licensure status.

### FAQ

#### Does this Actor search the Board's individual verification portal?

No.
It normalizes the official bulk active-status roster files linked from the Board's roster page.

#### Are inactive licenses included?

No.
The source files are explicitly published as active-status rosters.

#### Are MD, PA, and DPM values mixed together?

They share one dataset, but `profession` and `professionCode` identify every row.

#### Can I identify changes between months?

Yes, by scheduling runs and comparing datasets externally.
Use the profession, credential type, and credential number as a practical composite key.

#### Is a no-result search charged per item?

No item charge is generated when no record is saved.
The one-time run-start charge still applies.

#### Does the Actor use a proxy?

No.
The current official roster and XLSX downloads are accessible through direct HTTP requests.

#### Is the data real time?

No.
The Board's roster page states the publication date and says rosters are updated monthly.
Each result preserves that publication date so users can judge freshness.

# Actor input Schema

## `professions` (type: `array`):

Professions to include. Leave all selected for the complete medical doctor, physician assistant, and podiatric physician roster.

## `credentialTypes` (type: `array`):

Official active-status roster worksheet types to include. The default license selection returns standard licenses.

## `searchTerm` (type: `string`):

Optional case-insensitive text matched against full practitioner name or credential number after downloading the official roster.

## `maxItems` (type: `integer`):

Maximum matching records saved to the dataset across all selected rosters.

## Actor input object example

```json
{
  "professions": [
    "MD",
    "PA",
    "DPM"
  ],
  "credentialTypes": [
    "license"
  ],
  "searchTerm": "Abadir",
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Default dataset containing normalized license, permit, and registration records.

# 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 = {
    "professions": [
        "MD",
        "PA",
        "DPM"
    ],
    "credentialTypes": [
        "license"
    ],
    "searchTerm": "Abadir",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/west-virginia-physician-license-roster").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 = {
    "professions": [
        "MD",
        "PA",
        "DPM",
    ],
    "credentialTypes": ["license"],
    "searchTerm": "Abadir",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/west-virginia-physician-license-roster").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 '{
  "professions": [
    "MD",
    "PA",
    "DPM"
  ],
  "credentialTypes": [
    "license"
  ],
  "searchTerm": "Abadir",
  "maxItems": 20
}' |
apify call automation-lab/west-virginia-physician-license-roster --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/west-virginia-physician-license-roster"
        }
    }
}

```

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/69sMEmxUixAGS2ltj/builds/4WJFJ2PGbXb3PDTYt/openapi.json
