# Florida Medical License Scraper — DOH MQA Verification (`scrapersdelight/florida-medical-license-scraper`) Actor

Primary-source verification and bulk extraction of Florida's 3.48M health care licenses from the DOH/MQA portal: physicians, nurses, dentists, pharmacists and 32 more boards. Paste license numbers to verify, or filter by board, profession, county or ZIP. Status, address, expiry, discipline.

- **URL**: https://apify.com/scrapersdelight/florida-medical-license-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 per licensee record returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Florida Medical License Scraper — DOH MQA Verification

> **Every licensed health care practitioner in Florida, from the state's own primary source.**
> Paste a list of license numbers and get them verified one by one, or pull a whole board, profession
> or county in bulk. One row per licensee: **license number, name, profession, board, license status,
> address of record, original issue date, expiration date, days-until-expiry, controlled-substance
> prescriber flag, cannabis-order authority, and the full disciplinary and public-complaint history
> with case numbers and document IDs** — plus **e-mail, specialty board certifications, hospital staff
> privileges, Medicaid participation and other-state licenses** on physicians.
> **$0.003 per licensee — $3 per 1,000.** No login, no API key, no CAPTCHA, no proxy needed.

Source: the Florida Department of Health's
[Medical Quality Assurance license verification portal](https://mqa-internet.doh.state.fl.us/MQASearchServices/HealthCareProviders)
— the state's designated **primary source** for license verification, which is the standard a
credentialing file has to meet.

**3,481,761 license records across 36 boards and 237 professions**, measured 2026-08-12 as the sum of
the portal's own authoritative per-board totals — not quoted from a marketing page. Filter by board,
profession, county, city, ZIP, last name, first name, business name or license status.

```json
{
  "board": "15",
  "profession": "1501",
  "county": "13",
  "detailLevel": "license",
  "maxItems": 100
}
```

Click **Try for free** and hit **Start** — that is the input the Actor ships with. Measured
2026-08-12: **100 practicing Bay County physicians, full license depth, 29 seconds, coverage verified
100.0% against the portal's own roster, $0.30.**

Calling it from the API, an MCP client or an agent with an **empty input (`{}`)** does not fail: the
run falls back to that same sample search — capped at 100 licensees, $0.30 — and the log and run
status name the fields to set for your own search. Nothing is charged when a search matches nothing.

***

### What this Actor does

Florida MQA licenses essentially every regulated health care occupation in the state — physicians,
osteopaths, physician assistants, registered and practical nurses, CNAs, APRNs, dentists, dental
hygienists, pharmacists, pharmacy technicians and pharmacy facilities, massage therapists, physical
and occupational therapists, psychologists, clinical social workers, marriage and family therapists,
mental health counselors, chiropractors, optometrists, podiatrists, acupuncturists, speech-language
pathologists and audiologists, respiratory therapists, dietitians, athletic trainers, EMTs and
paramedics, EMS provider organizations, clinical laboratory personnel, opticians, orthotists and
prosthetists, genetic counselors, midwives, electrologists, hearing aid specialists, nursing home
administrators, radiation-control licensees, 911 telecommunicators and out-of-state telehealth
providers.

It serves two jobs from one input form:

1. **Primary-source verification.** Paste the license numbers you already hold. Each resolves
   straight to its own DOH verification page, all statuses searched, so a revoked or null-and-void
   license still verifies. Numbers that match nothing are named in the log, written to the key-value
   store, and never charged.
2. **Bulk extraction.** Pick a board, profession and location and pull the slice — at roster depth
   the whole slice arrives in a single request.

***

### Why scrape MQA yourself when this exists?

Every one of these is a measured behaviour of the live portal, not a hypothetical:

- **The portal's CSV export truncates silently.** Over a certain slice size it returns HTTP 200, a
  correct `text/csv` content-type, a valid header row — and **zero data rows**. A naive scraper
  reports that as a successful empty run. Numbers below.
- **The obvious pagination fix is the bug.** The results grid offers a "sort by license number" link
  and pinning it looks like the way to make offset paging stable. The portal discards that re-sorted
  set after about a minute and reverts to name order mid-walk, silently dropping licensees.
- **Neither county nor license status partitions the data.** Filtering by all 67 counties reaches
  55.3% of physicians; the state's own two status buckets reach 99.78%. Split a big slice on either
  axis and you lose rows without being told.
- **Search results live in the session**, not the query string — `/IndexPaged?page=N` is driven by a
  cookie, so a concurrent second search corrupts the first one's walk.
- **A profession code only works with its own board**, and a mismatched pair returns no result count
  at all rather than an error.
- **Address of record is withheld on non-practicing licenses** and the portal substitutes a
  "contact the Department" notice in the same slot — parse it naively and you ship that sentence as
  a street address.
- **Deceased licensees carry no address block at all**, a different shape again.
- **Dates arrive as both `M/D/YYYY` and `MM/DD/YYYY`**, and the verification page prints some
  statuses with a dangling separator ("Null And Void/") where the CSV export prints "Null And Void".
- **Discipline lives on a separate lazy-loaded tab**, keyed by an internal `LicInd` you only get from
  the results grid, and the document PDFs are behind a POST with an opaque base64 document key.
- **Some professions have no practitioner profile at all.** Florida publishes it for physicians, PAs
  and ARNPs only — a scraper that promises e-mails on nurses is promising a field the state does not
  publish.

***

### Quick start

1. Open the Actor and click **Try for free**.
2. Either paste license numbers into **License numbers to verify**, or leave the input exactly as it
   ships (Board of Medicine → Medical Doctor → Bay County → license depth → 100 rows).
3. Hit **Start**. The default run finishes in about 30 seconds.
4. Open the **Dataset** tab and export as JSON, CSV, Excel, XML or JSONL.

Time to first data: **under a minute, no coding**.

#### Verify one license in a single HTTP call (Python)

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("scrapersdelight/florida-medical-license-scraper").call(run_input={
    "licenseNumbers": ["ME58861", "RN9268431", "ME68270"],
    "detailLevel": "profile",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["licenseNumber"], row["licenseStatus"], row["licenseExpirationDate"],
          row["disciplineOnFile"], row["email"])
```

#### Bulk pull (Node.js / TypeScript)

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('scrapersdelight/florida-medical-license-scraper').call({
    profession: '1711',        // Advanced Practice Registered Nurse
    county: '23',              // Miami-Dade
    licenseStatus: 'ACT',
    detailLevel: 'license',
    maxItems: 2000,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(`${items.length} practicing Miami-Dade APRNs`);
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/scrapersdelight~florida-medical-license-scraper/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"board":"17","profession":"1701","city":"ORLANDO","detailLevel":"roster","maxItems":5000}'
```

#### The killer one-liner: synchronous single-license verification

```bash
curl -X POST "https://api.apify.com/v2/acts/scrapersdelight~florida-medical-license-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"licenseNumber":"ME58861","detailLevel":"profile"}'
```

The finished record comes back in the HTTP response. That is a credentialing check as a single API
call, billed at $0.003, with no polling and no webhook.

***

### The wedge: this is the primary source, and it carries the discipline file

Every commercial provider-data product — Healthgrades, Vitals, Doximity, an NPI-derived list — is a
*secondary* source. A credentialing file that has to withstand an audit needs the state board itself.
This Actor reads the board itself, and it returns the two things the aggregators do not have:

**1. The disciplinary and public-complaint file, structured.** Every discipline case, emergency action
and public complaint on file, each with its case number, the action taken, and the portal's own
document key:

```json
"disciplinaryActions": [
  { "type": "Discipline Case",  "caseNumber": "201816816",
    "actionTaken": "Obligation(s) Satisfied", "documentId": "Mzg5Mzk4NDQ=" },
  { "type": "Public Complaint", "caseNumber": "201816816",
    "actionTaken": "AC Filed",                "documentId": "MzcxMzc3MzI=" }
]
```

Actions seen across the measured runs: *AC Filed · Obligation(s) Satisfied · Obligations Imposed ·
Revocation · Voluntary Surrender · Suspension Satisfied · Fine · Letter of Concern*. `documentId` is
the portal's own key — POST it as `id` to `/MQASearchServices/Document` to pull the signed order PDF.

Measured rarity: **7 of 100** practicing Bay County physicians and **3 of 200** practicing Gainesville
physicians carried at least one enforcement entry; on a 300-licensee all-statuses Bay County run,
**26 of 300** did. A discipline flag is rare, which is exactly why the buyers who need it cannot
sample for it — they need the whole file.

**2. The status and expiry that decide whether someone may legally work today.** `licenseStatus`,
`isPracticing`, `licenseExpirationDate` and `daysUntilExpiry` were 100% filled on both measured runs.
That is a renewal-reminder product and a network-integrity product in four fields.

***

### Read this before you buy rows

**1. An address of record exists only for practicing licenses.** Florida withholds it for
Null-and-Void, Retired, Revoked, Deceased and Voluntarily-Relinquished licenses — and for a small
number of otherwise-active ones — and prints a "contact the Department" notice instead. This Actor
never passes that notice off as a street address: it sets `addressWithheld: true` and leaves the
address fields null. DOH uses **two** wordings for that notice ("further information is needed…" and
"This practitioner does not have an address of record on file with the department…"); both are
detected as of 2026-08-19. The 300-licensee table below was measured before the second wording was
handled, so its `address` column counts a handful of notice rows as addresses — the 100-row table in
the next section is the post-fix measurement.

Measured on a full 300-licensee Bay County physician run, all statuses:

| | rows | address | ZIP | expiry date |
|---|---:|---:|---:|---:|
| **Practicing** (`Clear/Active` etc.) | 183 | **100%** | **97%** | **100%** |
| Non-practicing (Null & Void, Deceased, Retired, Revoked…) | 117 | 9% | 8% | 65% |

That is why **License status defaults to Practicing only**. Switch it to All statuses when you are
credentialing and need to see the bad ones — a license-number lookup always searches all statuses
anyway, so a revoked license still verifies.

**2. A county filter silently excludes about 45% of licensees.** Only about 55% of Florida licensees
have a Florida county on file; the rest hold a Florida license at an out-of-state address. Measured:
the 67 counties sum to 96,534 of 174,569 physicians = **55.3%**. Filter by county only when you
actually mean "practises in this county". `county` is echoed onto each row when you filter by it —
DOH prints no county anywhere in its output, so the field is null otherwise (measured 0% on the
unfiltered Gainesville run).

**3. The practitioner profile — e-mail, specialty, hospitals — exists only for physicians.** Florida
publishes it for MDs, DOs, PAs and ARNPs, not for nurses, dentists or pharmacists. On those,
`detailLevel: "profile"` costs nothing extra and simply leaves those fields null.

**4. `lastName` and `firstName` are null on two kinds of row, deliberately.** On the license-number
path, the verification page prints the name in display order only ("PATRICIA LIPFORD ABBITT") with no
comma to split on, and this Actor will not guess a surname out of "JOHN VAN DER BERG". On facility
licenses the comma is a corporate suffix — "Holiday CVS, LLC" is a company, not a person called LLC.
In both cases `name` holds exactly what DOH printed. Search-driven runs on people (board /
profession / county) do get the comma form and were split 100% on both measured runs.

**5. `board` and `boardCode` are null when you did not filter by board.** DOH does not print the
board on the verification page; it is known only from the search you ran. The profession is always
present and is the finer-grained field anyway.

***

### What you get — one row per licensee

**Identity**

| Field | Type | Notes |
|---|---|---|
| `licenseNumber` | string | e.g. `ME58861`, `RN9268431`. The dedupe key, with `professionCode`. |
| `name` | string | Exactly as DOH printed it. `LAST, FIRST MIDDLE` on search runs, display order on license-number lookups. |
| `lastName` / `firstName` | string | null | Split only when DOH supplies the comma form — see gotcha 4. |
| `profession` | string | e.g. `Medical Doctor`, `Registered Nurse`. |
| `professionCode` | string | e.g. `1501`, `1701`. |
| `board` / `boardCode` | string | null | Populated when you filtered by board. |

**Status and dates** — all dates are ISO-8601 `YYYY-MM-DD`

| Field | Type | Notes |
|---|---|---|
| `licenseStatus` | string | `Clear/Active`, `Null And Void`, `Retired`, `Revoked`, `Probation/Active`, `Military Active`, `Obligations/Active`, `Conditional/Active`, `Clear/Inactive`, `Vol Relinquish`, `Delinquent`, `Deceased`… |
| `isPracticing` | boolean | True for the five statuses the portal itself counts as practicing. |
| `licenseOriginalIssueDate` | date | |
| `licenseExpirationDate` | date | |
| `daysUntilExpiry` | integer | Negative when already expired. Computed at scrape time. |

**Address of record**

| Field | Type | Notes |
|---|---|---|
| `addressLine1` / `addressLine2` | string | null | |
| `city` / `state` / `zip` | string | null | |
| `county` | string | null | Echoed from your filter; DOH never prints it. |
| `addressWithheld` | boolean | `true` = DOH published a "contact the Department" notice instead of an address. |

**Regulatory**

| Field | Type | Notes |
|---|---|---|
| `disciplineOnFile` / `publicComplaint` | boolean | The portal's own flags. |
| `disciplinaryActions` | array | null | `{type, name, license, profession, city, state, caseNumber, actionTaken, documentId}` |
| `controlledSubstancePrescriber` | boolean | Chronic non-malignant pain prescriber flag. |
| `authorizedToOrderCannabis` | boolean | null | Null means the portal did not render the row at all — see limits. |
| `qualifications` | string | null | e.g. `Dispensing Practitioner`. |

**Practitioner profile** *(physicians, PAs and ARNPs only; `detailLevel: "profile"`)*

| Field | Type | Notes |
|---|---|---|
| `email` | string | null | The address the practitioner published for contact. |
| `specialtyCertifications` | array | null | `{specialtyBoard, certification, dateCertified}` |
| `hospitalPrivileges` | array | null | `{institution, city, state}` |
| `otherStateLicenses` | array | null | `{state, profession}` |
| `medicaidParticipant` | boolean | null | |
| `practiceAddress` | string | null | |
| `yearBeganPracticing` | string | null | As DOH prints it on the profile — usually a full date (`07/01/1996`), sometimes blank. |
| `profileAvailable` | boolean | Whether DOH publishes a profile for this licensee at all. |

**Provenance**

| Field | Type | Notes |
|---|---|---|
| `licenseVerificationUrl` | string | The exact DOH page this row came from — put it in the credentialing file. |
| `sourceUrl` / `scrapedAt` | string | ISO-8601 UTC timestamp. |
| `detailLevel` | string | The depth this row was actually built at. |

**Key-value store artifact:** `NOT_FOUND_LICENSE_NUMBERS` — the license numbers from a batch run that
matched nothing. Written only when there are some.

***

### Field fill — measured, not estimated

Every run prints its own measured fill in the log. These two tables are from real runs on 2026-08-12.

#### A. License depth — 100 practicing Bay County physicians (29 s)

| Field | Fill |
|---|---:|
| `licenseNumber`, `name`, `lastName`, `firstName`, `profession`, `professionCode`, `board`, `boardCode`, `county` | **100%** |
| **`licenseStatus`**, `isPracticing`, **`licenseExpirationDate`**, `licenseOriginalIssueDate`, `daysUntilExpiry` | **100%** |
| `city` | **100%** |
| `disciplineOnFile`, `publicComplaint`, `controlledSubstancePrescriber`, `addressWithheld`, `profileAvailable` | **100%** |
| **`addressLine1`**, `zip`, `state` | 98% |
| `addressLine2` | 32% |
| `qualifications` | 22% |
| **`disciplinaryActions`** | **7%** |
| `authorizedToOrderCannabis` | 6% |
| `email`, `specialtyCertifications`, `hospitalPrivileges`, `otherStateLicenses`, `medicaidParticipant` | 0% *(license depth does not fetch the profile)* |

Boolean breakdown on the same 100 rows: `controlledSubstancePrescriber` 29 true / 71 false ·
`disciplineOnFile` 6 true / 94 false · `publicComplaint` 6 true / 94 false · `addressWithheld` 2 true
/ 98 false. Seven rows carried at least one enforcement entry.

#### B. Profile depth — 200 practicing Gainesville physicians (67 s)

| Field | Fill |
|---|---:|
| Identity, status, dates, `addressLine1`, `city`, `practiceAddress` | **100%** |
| `zip`, `state` | 98% |
| **`email`** | **88%** |
| `medicaidParticipant` | 66% |
| `yearBeganPracticing` | 63% *(re-measured 2026-08-19 on 30 Gainesville physicians; 0% before that build — see changelog)* |
| `hospitalPrivileges` | 57% |
| `specialtyCertifications` | 50% |
| `otherStateLicenses` | 50% |
| `addressLine2` | 48% |
| `qualifications` | 28% |
| `disciplinaryActions` | 2% |
| `authorizedToOrderCannabis` | 0% |
| `county` | 0% *(no county filter was applied)* |

`medicaidParticipant` on those 200: 56 true, 76 false, 68 null — null means DOH rendered no Medicaid
statement, which is not the same as "does not participate".

**The headline that could mislead you: e-mail is a physician field, not a Florida field.** 88% on
practicing Gainesville physicians; 0% on nurses, dentists, pharmacists and every other board, because
Florida does not publish a practitioner profile for them. Plan against the table that matches the
board you are actually pulling.

***

### Three depths

| `detailLevel` | Requests | What you get |
|---|---|---|
| **`roster`** | **1 for the whole slice** | license number, name, profession, city, status |
| **`license`** (what the form ships prefilled with) | ~1 per licensee | + address of record, original issue date, expiry, days-until-expiry, controlled-substance flag, cannabis-order authority, qualifications, **disciplinary actions & public complaints** |
| **`profile`** | ~2 per licensee | + e-mail, specialty board certifications, hospital staff privileges, Medicaid participation, other-state licenses, practice address *(physicians, PAs and ARNPs only)* |

**The price per licensee is identical at all three depths.** Depth buys you fields and costs you
time, never money.

`roster` reads the portal's own bulk export, so **13,853 Miami physicians came back in a single
request** — verified 100% against the portal's authoritative count. Use it to size a job or to build
a license-number list, then re-run the interesting subset at `license` or `profile` depth.

***

### How to run it

#### Verify a batch of licenses you already hold

```json
{
  "licenseNumbers": ["ME58861", "RN9268431", "ME68270"],
  "detailLevel": "profile"
}
```

One search request per number, all statuses. Measured 2026-08-12: 4 numbers in, 3 resolved, 1 bogus
number reported and not charged, **7 seconds**. Verifying 500 numbers in one run costs exactly the
same as 500 single-number runs and takes a fraction of the wall time — but **`maxItems` caps the
batch as well as a search, and it ships at 100**, so raise it to at least the length of your list
(`"maxItems": 500`) or the run stops at the cap and says so in the log.

#### Pull a profession statewide, cheaply

```json
{ "profession": "1711", "licenseStatus": "ACT", "detailLevel": "roster", "maxItems": 70000 }
```

Every practicing APRN in Florida — 69,531 of them as of 2026-08-12 — in one request at roster depth.
Leave `board` on "Any board": a profession is unique across boards, so the board filter adds nothing.
`maxItems` has no wildcard: set it above the slice size you want, and the run stops at whichever
comes first.

#### Slice a metro at full depth

```json
{ "board": "15", "profession": "1501", "county": "23", "licenseStatus": "ACT",
  "detailLevel": "license", "maxItems": 2000 }
```

10,903 practicing Miami-Dade physicians match (measured 2026-08-12); this takes the first 2,000.

#### Renewal monitoring on a schedule

```json
{ "profession": "1701", "county": "58", "licenseStatus": "ACT", "detailLevel": "license",
  "maxItems": 5000 }
```

Then filter on `daysUntilExpiry` between 0 and 90 in your own pipeline, and put the run on an Apify
Schedule. `daysUntilExpiry` is computed at scrape time, so it is only as fresh as the run.

#### Facility licenses rather than people

```json
{ "profession": "2205", "businessName": "CVS", "licenseStatus": "ACT", "detailLevel": "license" }
```

27,040 pharmacy-facility licenses exist, of which 11,036 are practicing; a `businessName` of `CVS`
narrows that to 780 (all measured 2026-08-12). Those rows carry a business name in `name` and no
person name.

***

### Sample rows

#### License depth, with a discipline file — a real row from the default run

```jsonc
{
  "licenseNumber": "ME68270",
  "name": "ADELBERG, JONATHAN MICHAEL",
  "lastName": "ADELBERG",
  "firstName": "JONATHAN MICHAEL",

  "profession": "Medical Doctor",
  "professionCode": "1501",
  "board": "BOARD OF MEDICINE",
  "boardCode": "15",

  "licenseStatus": "Clear/Active",
  "isPracticing": true,
  "licenseOriginalIssueDate": "1995-05-08",
  "licenseExpirationDate": "2027-01-31",
  "daysUntilExpiry": 171,

  "addressLine1": "2424 Frankford Avenue",
  "addressLine2": "Unit A",
  "city": "PANAMA CITY",
  "state": "FL",
  "zip": "32405",
  "county": "BAY",
  "addressWithheld": false,

  "controlledSubstancePrescriber": true,
  "authorizedToOrderCannabis": null,
  "qualifications": "Dispensing Practitioner",
  "disciplineOnFile": true,
  "publicComplaint": true,
  "disciplinaryActions": [
    { "type": "Discipline Case", "name": "ADELBERG, JONATHAN MICHAEL", "license": "68270",
      "profession": "Medical Doctor", "city": "PANAMA CITY", "state": "FL",
      "caseNumber": "201816816", "actionTaken": "Obligation(s) Satisfied",
      "documentId": "Mzg5Mzk4NDQ=" },
    { "type": "Public Complaint", "name": "ADELBERG, JONATHAN MICHAEL", "license": "68270",
      "profession": "Medical Doctor", "city": "PANAMA CITY", "state": "FL",
      "caseNumber": "201816816", "actionTaken": "AC Filed",
      "documentId": "MzcxMzc3MzI=" }
  ],

  "profileAvailable": true,
  "detailLevel": "license",
  "licenseVerificationUrl": "https://mqa-internet.doh.state.fl.us/MQASearchServices/HealthCareProviders/LicenseVerification?LicInd=59151&ProCde=1501",
  "scrapedAt": "2026-08-13T02:05:12.772Z"
}
```

#### Profile depth — a real row from the batch verification run

```jsonc
{
  "licenseNumber": "ME58861",
  "name": "PATRICIA LIPFORD ABBITT",
  "lastName": null,
  "firstName": null,

  "profession": "Medical Doctor",
  "professionCode": "1501",
  "board": null,
  "boardCode": null,

  "licenseStatus": "Clear/Active",
  "isPracticing": true,
  "licenseOriginalIssueDate": "1990-12-17",
  "licenseExpirationDate": "2028-01-31",
  "daysUntilExpiry": 536,

  "addressLine1": "DEPT OF RADIOLOGY - UF COM",
  "addressLine2": "1600 SW ARCHER RD - RM G-304",
  "city": "GAINESVILLE",
  "state": "FL",
  "zip": "32610",
  "addressWithheld": false,

  "controlledSubstancePrescriber": false,
  "disciplineOnFile": false,
  "publicComplaint": false,
  "disciplinaryActions": null,

  "email": "abbitp@radiology.ufl.edu",
  "medicaidParticipant": true,
  "specialtyCertifications": [
    { "specialtyBoard": "AMERICAN BOARD OF RADIOLOGY",
      "certification": "DR - DIAGNOSTIC RADIOLOGY", "dateCertified": null }
  ],
  "hospitalPrivileges": [
    { "institution": "SHANDS HOSPITAL AT THE UNIVERSITY OF FLO",
      "city": "GAINESVILLE", "state": "FLORIDA" }
  ],
  "otherStateLicenses": [{ "state": "VIRGINIA", "profession": null }],
  "practiceAddress": "PATRICIA LIPFORD ABBITT, DEPT OF RADIOLOGY - UF COM, 1600 SW ARCHER RD - RM G-304, GAINESVILLE, FL 32610",
  "yearBeganPracticing": null,

  "detailLevel": "profile",
  "licenseVerificationUrl": "https://mqa-internet.doh.state.fl.us/MQASearchServices/HealthCareProviders/LicenseVerification?LicInd=49786&ProCde=1501"
}
```

Fields people misread:

- `lastName: null` on this row is the license-number path, not missing data — `name` is complete.
- `board: null` means you did not filter by board, not that the licensee has none.
- `authorizedToOrderCannabis: null` means the portal rendered no such row; `false` means it rendered
  one saying no. They are different facts.
- `medicaidParticipant: null` means DOH published no Medicaid statement, not "does not participate".
- `daysUntilExpiry` is negative for an already-expired license.
- `disciplinaryActions[].license` is the bare number DOH prints in the enforcement table (`68270`),
  without the `ME` prefix that `licenseNumber` carries.

***

### Input

Fifteen fields in six groups, every one of them carrying a default, in the same order as the form.
Two things worth separating: the **form ships prefilled** with Board of Medicine → Medical Doctor →
Bay County → license depth → 100 rows, which is the run you get by pressing Start; the **Default**
column below is what applies when a field is simply omitted from an API call.

| Field | Type | Default | What it does |
|---|---|---|---|
| `licenseNumbers` | string list | `[]` | Batch primary-source verification. One request per number, all statuses. Unmatched numbers are logged, saved to `NOT_FOUND_LICENSE_NUMBERS` and never charged. |
| `licenseNumber` | text | `""` | Single lookup — identical to one entry above. The run-sync API path. |
| `board` | select (36) | Any | Florida MQA board or council. Sizes in the table below. |
| `profession` | select (237) | Any | Each option is labelled with the board that owns it. Unique across boards, so it can be used alone. |
| `professionCustom` | text | `""` | Escape hatch — a profession name or code not in the dropdown. Overrides it. |
| `county` | select (67) | Any | **Costs you ~45% of the population** — see gotcha 2. |
| `city` | text | `""` | Exact match on DOH's spelling, e.g. `GAINESVILLE`. |
| `zipCode` | text | `""` | 5-digit ZIP. Tightest geographic filter, and the cheapest way to keep a deep run small. |
| `lastName` | text | `""` | Prefix match. Also the axis the Actor splits oversized slices on. |
| `firstName` | text | `""` | Prefix match. |
| `businessName` | text | `""` | Prefix match for facility licenses (pharmacies, labs, EMS providers). |
| `detailLevel` | select | `roster` | `roster` · `license` · `profile`. Same price at every depth. The form is prefilled with `license`. |
| `licenseStatus` | select | `ACT` | Practicing only, non-practicing only, or all. Ignored on license-number lookups, which always search all statuses. |
| `maxItems` | integer | `100` | Row cap **and** your billing cap. `0` is not a wildcard — set a number. |
| `proxyConfiguration` | proxy | off | Leave off. Measured ladder below. |

#### The 36 boards, with the size of each (measured 2026-08-12)

| Code | Board | Records |
|---|---|---:|
| 17 | BOARD OF NURSING | 1,573,814 |
| 22 | BOARD OF PHARMACY | 272,596 |
| 15 | BOARD OF MEDICINE | 269,685 |
| 25 | BUREAU OF EMERGENCY MEDICAL SERVICES | 258,162 |
| 76 | RADIATION CONTROL | 179,419 |
| 14 | BOARD OF MASSAGE THERAPY | 152,118 |
| 52 | BOARD OF CSW/MFT/MHC | 122,492 |
| 7 | BOARD OF DENTISTRY | 105,285 |
| 66 | BOARD OF CLINICAL LABORATORY PERSONNEL | 77,360 |
| 55 | BOARD OF PHYSICAL THERAPY PRACTICE | 62,806 |
| 30 | SPEECH-LANGUAGE PATHOLOGY AND AUDIOLOGY | 50,979 |
| 57 | BOARD OF RESPIRATORY CARE | 44,975 |
| 96 | OUT-OF-STATE TELEHEALTH PROVIDERS | 43,552 |
| 56 | BOARD OF OCCUPATIONAL THERAPY PRACTICE | 38,928 |
| 5 | BOARD OF CHIROPRACTIC MEDICINE | 37,149 |
| 19 | BOARD OF OSTEOPATHIC MEDICINE | 35,258 |
| 26 | EMS PROVIDERS | 29,739 |
| 24 | PUBLIC SAFETY TELECOMMUNICATIONS | 22,688 |
| 20 | BOARD OF OPTICIANRY | 17,563 |
| 61 | DIETETICS AND NUTRITION PRACTICE COUNCIL | 15,682 |
| 27 | BOARD OF PSYCHOLOGY | 11,666 |
| 18 | BOARD OF OPTOMETRY | 10,538 |
| 65 | ELECTROLYSIS COUNCIL | 8,951 |
| 21 | BOARD OF PODIATRIC MEDICINE | 8,296 |
| 10 | BOARD OF ATHLETIC TRAINING | 7,859 |
| 8 | NURSING HOME ADMINISTRATORS | 5,706 |
| 36 | BOARD OF HEARING AID SPECIALISTS | 5,549 |
| 38 | BOARD OF ACUPUNCTURE | 4,750 |
| 31 | BOARD OF ORTHOTISTS AND PROSTHETISTS | 2,326 |
| 41 | BOARD OF SCHOOL PSYCHOLOGY | 2,025 |
| 60 | ADVISORY COUNCIL OF MEDICAL PHYSICIST | 1,981 |
| 53 | GENETIC COUNSELING | 982 |
| 32 | COUNCIL OF LICENSED MIDWIFERY | 524 |
| 62 | HEALTH CARE SERVICES POOLS | 279 |
| 16 | BOARD OF NATUROPATHIC MEDICINE | 56 |
| 54 | CERTIFIED SOCIAL WORKERS | 23 |

**Total: 3,481,761.**

#### The professions worth knowing the size of (measured 2026-08-12)

| Code | Profession | All statuses | Practicing |
|---|---|---:|---:|
| 1701 | Registered Nurse | 790,880 | 395,219 |
| 4401 | Certified Nursing Assistant | 484,858 | 152,572 |
| 1702 | Licensed Practical Nurse | 213,475 | 62,469 |
| 1501 | Medical Doctor | 174,575 | 91,039 |
| 2501 | Emergency Medical Technician | 161,484 | 50,538 |
| 2208 | Registered Pharmacy Technician | 137,615 | 59,709 |
| 1401 | Massage Therapist | 107,519 | 36,723 |
| 1711 | Advanced Practice Registered Nurse | 82,864 | 69,531 |
| 2502 | Paramedic | 65,416 | 39,928 |
| 2201 | Pharmacist | 60,144 | 37,204 |
| 5501 | Physical Therapist | 41,387 | 21,678 |
| 702 | Dental Hygienist | 32,905 | 19,225 |
| 701 | Dentist | 30,279 | 18,638 |
| 2205 | Pharmacy (facility) | 27,040 | 11,036 |
| 5601 | Occupational Therapist | 25,482 | 12,996 |
| 3001 | Speech-Language Pathologist | 24,868 | 13,924 |
| 1512 | Physician Assistant | 23,833 | 15,673 |
| 1901 | Osteopathic Physician | 22,892 | 13,122 |
| 501 | Chiropractic Physician | 14,964 | 7,583 |
| 2701 | Psychologist | 11,262 | 7,009 |
| 1801 | Optometrist | 6,627 | 4,247 |
| 2101 | Podiatric Physician | 4,418 | 1,853 |

The dropdown carries all 237. Note how far "all statuses" sits above "practicing": more than half of
every registry in Florida is historical. The default keeps you on the practicing half.

***

### Pricing

**$0.003 per licensee returned — $3 per 1,000.** Charged on the `licensee-scraped` event. No monthly
platform fee from this Actor.

The same rate applies at every depth: a `roster` row, a `license` row with the full disciplinary file,
and a `profile` row with e-mail and hospital privileges all cost $0.003. Depth costs the Actor
requests, not you money.

| Run | Licensees | Cost |
|---|---:|---:|
| The default first click (Bay County physicians) | 100 | **$0.30** |
| Every practicing physician in Bay County | 460 | $1.38 |
| Every practicing physician in Gainesville | 2,326 | $6.98 |
| Every practicing physician in Miami-Dade | 10,903 | $32.71 |
| Every practicing physician in Florida | 91,039 | $273.12 |
| Every practicing APRN in Florida | 69,531 | $208.59 |

What you are **not** charged for:

- License numbers in a batch that match nothing. No row, no charge — and they are named in
  `NOT_FOUND_LICENSE_NUMBERS` so you know which.
- Duplicates. Rows are deduplicated on `licenseNumber` + `professionCode` **before** anything is
  pushed, so the dataset and your bill never hold the same licensee twice.
- The Actor's own retries, roster cross-checks, refinement splits, or the repair lookups it runs to
  reach 100% coverage. Those are requests, and requests are free to you.
- Rows above your cap. `maxItems` is a hard ceiling on both rows and spend.

Rows are charged as they are pushed (`Actor.pushData(items, 'licensee-scraped')`), so if you hit a
budget cap you get whole rows and a stop, never a half-billed dataset.

***

### Honest limits

- **No e-mail addresses outside physicians.** Florida publishes a practitioner profile for MDs, DOs,
  PAs and ARNPs only. Measured 88% e-mail fill on 200 practicing Gainesville physicians and **0% on
  every other board** — not because the scrape failed, but because the state does not publish it.
  If you need contactable nurses, dentists or pharmacists, this is not the product; take the
  `name` + `city` + `licenseNumber` from here and enrich elsewhere.
- **No phone numbers, for anyone.** MQA publishes an address of record and, for physicians, an
  e-mail. There is no phone field on the portal, so there is none here, and nothing can produce one
  from this source.
- **Address is a mailing address of record, not a practice location.** It is frequently a hospital
  department, a university mailroom, a billing service or a home. `practiceAddress` on profile-depth
  physician rows is closer to a practice location, but it is self-reported.
- **Non-practicing licensees are mostly blank.** 9% address fill, 8% ZIP, 65% expiry, measured on 117
  rows. That is DOH withholding, not a scraping failure. It also means a run with License status =
  All statuses returns a great many thin rows, and you are billed for them.
- **A county filter reaches 55.3% of the population.** Detailed above. Not a bug, a property of the
  state's data.
- **`authorizedToOrderCannabis` is null on the overwhelming majority of rows** — 94 of 100 on the Bay
  County run and 199 of 200 on the Gainesville run — because the portal renders that row only for
  the licensees it applies to. Do not read null as "not authorised".
- **Counts drift daily.** DOH refreshes nightly, and two reads of the same slice hours apart on
  2026-08-12 returned 174,569 and 174,575 physicians. Treat every count on this page as accurate to
  the day it was measured, not to the row.
- **`roster` depth cannot be filtered on anything the roster does not carry.** It returns license
  number, name, profession, city and status; if you need to filter on expiry or discipline you must
  pay for `license` depth first.
- **No login, no API key, no CAPTCHA solving, no browser automation.** The Actor reads public pages
  and the portal's own CSV export. It does not attempt any authenticated area, and there is nothing
  behind a login that it works around.

***

### How it works, and what was measured

The whole data path is public and unauthenticated:

| Step | Endpoint | Purpose |
|---|---|---|
| 1 | `GET /HealthCareProviders` | Session cookie, `__RequestVerificationToken`, and the live board / county / status dropdowns |
| 2 | `POST /HealthCareProviders` | Returns "Search Results Total: N" — **the authoritative count**, and binds the session to this result set |
| 3 | `GET /ExportToCsvLVP?jsonModel={…}` | Stateless bulk export — the whole result set as CSV in one request (174,569 rows / 13.7 MB in 31 s measured) |
| 4 | `GET /IndexPaged?page=N` | 20 rows per page, session-bound; yields the internal `LicInd` the detail pages are keyed by |
| 5 | `GET /LicenseVerification?LicInd=&ProCde=` | Address, issue and expiry dates, flags |
| 6 | `GET /LoadEnforcementActionsTab?…` | Discipline cases, emergency actions, public complaints |
| 7 | `GET /Details?LicInd=&ProCde=` | Practitioner profile (physicians only) |

The board, county and profession lists are re-read from the live form on **every run**, so a code
change on DOH's side cannot silently mis-map a search. The Actor keeps two separate cookie jars: the
search session drives `/IndexPaged`, and detail pages — which need neither cookie nor token — get
their own jar so nothing goes near the fragile paged walk.

#### Transport ladder — 18 identical calls per rung, measured through Apify

| Route | Success | Time |
|---|---|---|
| **Direct, no proxy** | **18/18 = 100%** | **28 s** |
| Apify automatic proxy | 16/18 = 88.9% | 298 s |
| Apify RESIDENTIAL, US, pinned session | 18/18 = 100% | 98 s |

The portal is public and unwalled. Proxying it only adds a failure mode — the two automatic-proxy
failures were `proxy responded with 590 UPSTREAM502` — and 3.5× the latency. **So the default is no
proxy at all**, which is also the cheapest rung there is. Detail pages run at concurrency 5, measured
30/30 successful at that level.

#### The silent-truncation trap

`ExportToCsvLVP` has an undocumented size ceiling and blows it silently: HTTP 200, correct
`text/csv` content-type, valid header row, zero data rows. Measured:

| Slice | Authoritative count | Rows the export returned |
|---|---:|---:|
| Medical Doctor, statewide | 174,569 | 174,569 ✅ |
| All of Dentistry | 105,284 | 105,284 ✅ |
| All of Pharmacy | 272,593 | **0** ❌ |
| Registered Nurse | 790,875 | **0** ❌ |
| All of Nursing | 1,573,806 | **0** ❌ |

So the row count is never trusted on its own. Every run reads the authoritative "Search Results Total"
first and reconciles against it. A short read is **retried** — a partial file is often just server
load; one slice came back 7,622 of 12,340 and then 6 of 6 clean retries returned all 12,340 — and if
the slice is genuinely oversized it is **split by last-name prefix**, with every leaf re-verified
against its own authoritative total.

Last name is the only axis that actually partitions the data. A–Z sums to **174,561 of 174,569
physicians (100.00%)** and **790,805 of 790,875 registered nurses (99.99%)**, where county sums to
55.3% and the state's own status buckets to 99.78% (ACT 91,034 + PREV 83,150 = 174,184 — four
statuses, Clear/Inactive among them, sit in neither bucket).

#### Pagination: the obvious fix is the bug

The results grid offers `/IndexSorted?fieldToSort=1` to order by license number, and the instinct is
to pin it so offset paging is stable. On this portal that is exactly backwards — the re-sorted result
set is a transient the server discards after about a minute, silently reverting to name order
mid-walk. Measured over one complete 38-page slice, 5 s between pages to imitate hydration latency,
checked against the CSV export of the same slice:

| | fetched | unique | lost |
|---|---:|---:|---:|
| Sort pinned | 758 | 743 | **15 licensees** |
| Default order (what this Actor uses) | 758 | **758** | **0** |

A fast walk hides it entirely — eight pages back-to-back produce zero duplicates either way — which
is what makes it a trap.

***

### Uniqueness and coverage

Rows are deduplicated on `licenseNumber` + `professionCode` before they are pushed, so a duplicate is
never delivered and never billed. Beyond that, `license` and `profile` runs do something most
scrapers do not: they reconcile the license numbers they hydrated against the authoritative CSV
roster for the same slice, and repair any straggler by direct license-number lookup.

Measured 2026-08-12 on contiguous page walks:

| Run | Expected | Emitted | Duplicates | Verified coverage |
|---|---:|---:|---:|---:|
| 100 practicing Bay County physicians, license depth | 100 | 100 | 0 | **100.0%** |
| 200 practicing Gainesville physicians, profile depth | 200 | 200 | 0 | **100.0%** |

That verified-coverage line is printed by every `license` and `profile` run whose slice was small
enough to cross-check against a complete CSV roster. A `roster` run stopped by `maxItems` prints the
cap it stopped at instead, and a license-number batch prints its own accounting — the resolved count
and the unmatched numbers by name. Across runs there is no cross-run dedupe — re-running the
same slice returns the same licensees. Use `lastName` prefixes or ZIP to partition a large job into
non-overlapping runs.

***

### When a run fails

The Actor is built to fail loudly rather than hand you a green, empty dataset:

- **Zero rows throws.** If the portal reported matches and the Actor extracted none, the run errors
  with the reported total instead of finishing successfully on an empty dataset.
- **A batch where nothing resolves throws**, and says so: "None of the N license numbers matched a
  Florida DOH/MQA licensee", with the reminder that DBPR numbers (`CGC…`, `CFC…`, `SL…`) belong to a
  different agency.
- **A missing verification token throws** rather than submitting a search that cannot work: "The page
  layout has changed — the Actor cannot submit a search without it."
- **An unknown board, profession or county throws with the live list of valid values**, read from the
  portal in that same run. An ambiguous partial name throws with the candidates it matched.
- **A profession that does not belong to the selected board does not throw** — the board is dropped
  with a warning, because the result set is provably identical either way.
- **A short read warns loudly** with the exact shortfall and tells you which filter to narrow.
- **A slice that stays short after three retries and three levels of prefix refinement throws**
  rather than shipping a partial directory as if it were complete.
- **Unmatched license numbers are never silent**: named in the log and written to
  `NOT_FOUND_LICENSE_NUMBERS` in the key-value store.

***

### Who buys this

- **Provider credentialing and primary-source-verification platforms** — Verifiable, Medallion,
  Certify, Silversheet and every hospital medical-staff office. `licenseStatus`, both dates,
  `disciplinaryActions` and `licenseVerificationUrl` are the PSV packet, and the URL is the citation
  the auditor asks for.
- **Payors and provider-network integrity teams** — `licenseStatus` plus `disciplinaryActions`
  plus `medicaidParticipant` is a monthly network sanction sweep. 6% of the practicing physicians in
  the measured run carried a discipline flag.
- **Healthcare staffing, travel nursing and locum agencies** — 395,219 practicing registered nurses
  and 69,531 practicing APRNs, sliceable by county and city, with expiry dates for renewal timing.
- **Medical device and pharma field teams** — the 29% of measured physicians flagged as
  controlled-substance prescribers, and the 57% with hospital staff privileges naming the institution,
  are a targeting layer Google Maps cannot give you.
- **Malpractice and professional-liability underwriters** — years since original issue, discipline
  history with case numbers, and other-state licensure on 50% of profile rows.
- **Healthcare M\&A and market researchers** — practitioner density by county and profession, from a
  registry of 3,481,761 records with a measured population per board.
- **Legal and investigative teams** — the case numbers and `documentId` keys lead to the signed order
  PDFs on the state's own document endpoint.

***

### Which of our Florida / healthcare Actors do you need?

| Actor | What it is | Why you would use it instead |
|---|---|---|
| **This one** | Florida **DOH / MQA** — health care practitioners | Doctors, nurses, dentists, pharmacists, therapists. Discipline file. Primary source. |
| [Florida Contractor License Scraper — DBPR](https://apify.com/scrapersdelight/florida-dbpr-license-scraper) | Florida **DBPR** — the *other* Florida licensing agency | Contractors, real estate, cosmetology, CPAs. A `CGC…` or `SL…` number belongs there, not here. |
| [NPI Registry Scraper](https://apify.com/scrapersdelight/npi-registry-scraper) | The federal NPPES/NPI registry, all 50 states | You need providers nationwide, or you need an NPI. MQA does not publish NPIs. |
| [Healthgrades Scraper](https://apify.com/scrapersdelight/healthgrades-scraper) | Consumer directory — doctors and dentists | You want ratings, reviews and phone numbers. Not a primary source, but it has the phone this one does not. |
| [Wellness.com Provider Scraper](https://apify.com/scrapersdelight/wellness-provider-scraper) | Consumer directory — practices and providers | Practice-level contact data and specialties. |

The short version: **Healthgrades is where you go for a phone number; this is where you go for the
license status, the expiry date and the disciplinary file.**

***

### Integrations

The dataset exports as JSON, CSV, Excel, XML and JSONL, and the Apify API serves it directly:

- **Google Sheets** — `IMPORTDATA` against the dataset's CSV endpoint for a live credentialing sheet.
- **Make / Zapier / n8n** — trigger on run finished, iterate items, push rows whose
  `daysUntilExpiry < 90` into a renewal queue.
- **Postgres / BigQuery / Snowflake** — load `licenseNumber` as the natural key; `scrapedAt` gives
  you the as-of timestamp every compliance table needs.
- **Salesforce / HubSpot** — match on `licenseNumber`, write `licenseStatus` and
  `licenseExpirationDate` back onto the provider record.
- **Slack / Teams alerts** — webhook on any row where `disciplineOnFile` flipped to `true` since
  your last pull.
- **Scheduling** — put a county or profession slice on an Apify Schedule and diff against your own
  store; the Actor has no monitor mode of its own.

***

### This Actor versus the alternatives

| | This Actor | Manual portal lookup | Your own scraper | A paid credentialing API |
|---|---|---|---|---|
| Setup | Minutes | None | Days, plus the seven traps above | Contract + onboarding |
| Bulk extraction | Yes, whole boards | No, one at a time | Yes, if you solve truncation and pagination | Usually per-lookup only |
| Disciplinary file | Structured, with document IDs | Manual tab-clicking | Extra work | Sometimes |
| Cost per 1,000 | **$3.00** | Staff time | Dev + proxy + maintenance | Typically per-seat or per-verification |
| Primary source | Yes — DOH itself | Yes | Yes | Often an aggregated copy |

***

### FAQ

**Does this need an account, a login or an API key?**
No. Every endpoint it reads is public and unauthenticated. There is no login and no CAPTCHA solving.

**Does it need a proxy?**
No, and it ships with the proxy off. Measured 18/18 direct versus 16/18 through Apify's automatic
proxy, at a third of the latency. Turn one on only if your own network policy needs a fixed egress IP.

**Can I verify a list of license numbers I already have?**
Yes — that is what `licenseNumbers` is for. One search request per number, all statuses searched, and
numbers that match nothing are listed in `NOT_FOUND_LICENSE_NUMBERS` and never charged.

**Is this the primary source, or a copy?**
It reads the Florida Department of Health's own MQA verification portal, which is the state's
designated primary source. Every row carries the exact `licenseVerificationUrl` it came from.

**Can I get the whole registry in one run?**
Not in one run, no. The whole index is 3,481,761 records across 36 boards, and the portal's CSV export
truncates silently on big slices: the largest that came back complete was 174,569 rows and the
smallest that came back empty was 272,593, so the ceiling sits somewhere between the two. The Actor
splits oversized slices by last-name prefix automatically, but a sane job is one board or one
profession at a time.

**Why is the e-mail field empty on my nurses?**
Because Florida does not publish a practitioner profile for nurses. It publishes one for physicians,
PAs and ARNPs — measured 88% e-mail fill there, 0% everywhere else.

**Why do so many rows have no address?**
You are looking at non-practicing licenses. Florida withholds the address of record on Null-and-Void,
Retired, Revoked and Deceased licenses: 9% address fill on those versus 100% on practicing ones.

**Do I get charged for rows my filters removed, or for the Actor's retries?**
No. You are billed per licensee actually delivered, on the `licensee-scraped` event. Retries, roster
cross-checks, refinement splits and repair lookups are requests, and requests are free to you.

**Two runs — will I get duplicates?**
Within a run, never: rows are deduplicated on `licenseNumber` + `professionCode` before they are
pushed, so a duplicate is never delivered or billed. Across runs there is no memory — re-running the
same slice returns the same licensees. Partition with `lastName` prefixes or ZIP.

**Will a run ever succeed with zero rows?**
No. If the portal reported matches and nothing was extracted, the run throws with the reported total
rather than finishing green on an empty dataset. A batch where no number resolves throws too.

**How fresh is the data?**
It is read live from the portal at run time. DOH refreshes its public database nightly; two reads of
the same slice hours apart on 2026-08-12 differed by six rows.

**What is `documentId` for?**
It is the portal's own document key for a disciplinary order. POST it as `id` to
`/MQASearchServices/Document` to retrieve the signed PDF.

**Does it cover Florida contractors, realtors or cosmetologists?**
No — those are licensed by DBPR, a different agency. Use our
[Florida Contractor License Scraper (DBPR)](https://apify.com/scrapersdelight/florida-dbpr-license-scraper).

**Something looks wrong — how do I debug it?**
A search run logs its criteria, the portal's authoritative match count, per-page progress and — when
the slice was small enough to cross-check against a full roster — the verified coverage percentage.
A license-number batch logs the resolved count and every unmatched number by name instead. Every run
ends with a per-field fill line. Compare those against the numbers on this page; if they diverge,
open an issue on the Issues tab with the run ID.

***

### Legal and fair use

This Actor reads **public records** published by the Florida Department of Health, Division of Medical
Quality Assurance, under Florida's public-records law. It reads only pages the portal serves to any
anonymous visitor: the search form, the search results, the portal's own CSV export, the license
verification page, the enforcement-actions tab and the practitioner profile. **It does not log in, it
holds no credentials, it solves no CAPTCHAs, and it collects nothing behind any authentication.**

Rows describe identifiable people and include names, mailing addresses, license history and, for
physicians, e-mail addresses and disciplinary records. **You are responsible for complying with the
Florida Department of Health's terms and with how you use the data**, including CAN-SPAM and TCPA if
you contact anyone, GDPR/UK GDPR if any data subject is in scope, and your own regulator's rules on
provider outreach. Disciplinary records are public record; using them in a decision about an
individual's credit, insurance, employment or housing may bring you under the FCRA, and this output
is **not a consumer report**. License status can change the moment after a row is scraped — every row
carries `licenseVerificationUrl` and `scrapedAt` so you can re-verify at the source before acting.

This Actor is an independent tool. It is **not affiliated with, endorsed by, or operated by** the
Florida Department of Health or the State of Florida.

***

### Changelog

| Version | Date | Change |
|---|---|---|
| 0.2.10 | 2026-08-20 | **Cost-limit handling.** The run now reads your "Maximum cost per run" up front and lowers its own row cap to what that budget pays for, so no licensee is ever scraped that could not be delivered — and it reads the charge result of every push, so it stops the moment the platform reports the cap reached instead of scraping on into a wall you are paying compute for. Rows returned and rows charged are the same number in every case; the run says which limit stopped it (cost or time), never both. Proof: a run capped at $0.05 returned **16 rows and charged 16** ($0.048), SUCCEEDED in 9.4 s with "Stopped at this run's maximum total charge with 16 licensee(s) delivered". **Memory right-sized 4096 MB -> 512 MB.** Compute bills linearly with memory, so this cuts the compute cost of every run by 8x at no change to the data. Peak memory measured across the heaviest runs (full 174,569-row statewide CSV; three 120-second unbounded runs) was 200.0 MiB — 2.6x headroom. |
| 0.2.6 | 2026-08-19 | **Address fix.** DOH publishes the address-withheld notice in two wordings and only one was detected, so rows carrying "This practitioner does not have an address of record on file with the department…" shipped that sentence as `addressLine1` with `addressWithheld: false` (and `"NOT PRACTICING"` as `addressLine2`). Both wordings are now detected. Measured on the same 100-row Bay County slice: `addressLine1` 100% -> 98%, `addressLine2` 34% -> 32%, `addressWithheld` 0 true -> 2 true, notice-in-address rows 2 -> 0. **`yearBeganPracticing` fix.** Its selector expected a bare `<dd>`; the profile page uses `<dd class="col-md-9">`, so it never matched — 0% on every profile run ever shipped. Now 63% (19/30 Gainesville physicians). The key is also declared on the record template, so every row carries the same field set. |
| 0.2 | 2026-08-12 | Batch `licenseNumbers` verification with unmatched-number reporting. Board, profession and county became dropdowns read from the live portal (36 / 237 / 67). Default depth is now `license`, default cap 100. `lastName`/`firstName` are null instead of wrong on license-number lookups. Field-fill, board-size and profession-size tables re-measured. |
| 0.1 | 2026-08-12 | First release. Verified against the live portal on 2026-08-12. |

***

### Keywords

Florida medical license lookup · Florida DOH license verification · MQA license search · Florida
nurse license lookup · Florida RN license verification · Florida physician license lookup · Florida
medical license scraper · primary source verification Florida · Florida provider credentialing data ·
Florida license expiration data · Florida disciplinary actions physicians · Florida board of medicine
data · Florida board of nursing data · Florida board of dentistry · Florida board of pharmacy ·
Florida APRN list · Florida CNA license lookup · Florida pharmacist license verification · Florida
dentist license lookup · Florida physical therapist license · Florida massage therapist license ·
Florida paramedic EMT license · Florida psychologist license · Florida chiropractor license ·
Florida optometrist license · Florida podiatrist license · Florida speech-language pathologist ·
Florida occupational therapist license · Florida healthcare provider list · Florida healthcare leads ·
Florida medical staffing data · locum tenens Florida · Miami-Dade physicians list · Broward physicians ·
Orlando nurses list · Tampa healthcare providers · Jacksonville physicians · Gainesville physicians ·
healthcare credentialing API · license verification API · provider network integrity · Medicaid
participation Florida · controlled substance prescriber Florida · hospital privileges data

***

### Support

Found a missing field, want a new filter, or need a board we do not surface well? Open an issue on the
**Issues** tab with your run ID and we will look at it.

If this Actor saved you time, a rating on the store page helps other people find it.

# Actor input Schema

## `licenseNumbers` (type: `array`):

One Florida DOH/MQA license number per line, e.g. ME58861, RN9268431, PS17155. Each number costs one search request plus its detail pages, and is billed as one licensee row only if it resolves. Verifying 500 numbers in ONE run costs exactly the same as 500 single-number runs and takes a fraction of the wall time — but raise "Max licensees" below to at least the length of your list, because it caps this batch too and ships at 100. These are DOH health-care numbers — Florida DBPR contractor and real-estate numbers (CGC…, CFC…, SL…) are a different agency and will not resolve here. Detail level "roster" has no meaning on this path: a license number resolves directly to its verification page, so these rows always come back at license depth (or profile, if you ask for it) at the same price per row.

## `licenseNumber` (type: `string`):

A one-off lookup, identical to putting a single value in the batch field above. Handy for run-sync API calls: POST /run-sync-get-dataset-items with {"licenseNumber":"ME58861","detailLevel":"profile"} returns the finished record in the HTTP response.

## `board` (type: `string`):

Which of Florida's 36 MQA boards and councils to search. The list is exactly the portal's own dropdown, read live on 2026-08-12. Board sizes measured the same day: Nursing 1,573,814 · Pharmacy 272,596 · Medicine 269,685 · Emergency Medical Services 258,162 · Radiation Control 179,419 · Massage Therapy 152,118 · CSW/MFT/MHC 122,492 · Dentistry 105,285. Leave on "Any board" if you are filtering by profession instead — a profession is unique across boards, so the board adds nothing.

## `profession` (type: `string`):

One of the 237 professions MQA licenses, each labelled with the board that owns it. Slice sizes measured 2026-08-12 (all statuses / practicing only): Registered Nurse 790,880 / 395,219 · Certified Nursing Assistant 484,858 / 152,572 · Licensed Practical Nurse 213,475 / 62,469 · Medical Doctor 174,575 / 91,039 · Emergency Medical Technician 161,484 / 50,538 · Registered Pharmacy Technician 137,615 / 59,709 · Massage Therapist 107,519 / 36,723 · Advanced Practice Registered Nurse 82,864 / 69,531 · Paramedic 65,416 / 39,928 · Pharmacist 60,144 / 37,204 · Physical Therapist 41,387 / 21,678 · Dental Hygienist 32,905 / 19,225 · Dentist 30,279 / 18,638. If the profession you pick belongs to a different board than the one above, the board filter is dropped and the run continues — the result set is identical either way (verified: board="" + Medical Doctor returns the same 174,575 as Medicine + Medical Doctor).

## `professionCustom` (type: `string`):

Escape hatch. Overrides the dropdown when filled. Accepts a profession NAME (partial and case-insensitive, e.g. "genetic counsel") or its numeric code. Use it if DOH adds a profession after this build — the Actor re-reads the live profession list on every run, so a new code works here immediately even though the dropdown above has not caught up.

## `county` (type: `string`):

One of Florida's 67 counties, read live from the portal. IMPORTANT: only about 55% of licensees carry a Florida county — the rest hold a Florida license at an out-of-state address — so a county filter deliberately drops them. Measured: the 67 counties sum to 96,534 of 174,569 physicians = 55.3%. Use it when you genuinely mean "practises in this county", not as a way to slice the state into 67 equal parts. The county you pick is echoed onto every row; DOH prints no county anywhere in its output, so the field is null on unfiltered runs.

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

City of the address of record, spelled as DOH stores it — MIAMI, ORLANDO, TAMPA, JACKSONVILLE, GAINESVILLE. Exact match, not a prefix. Empty = everywhere.

## `zipCode` (type: `string`):

5-digit ZIP of the address of record, e.g. 32610. The tightest geographic filter available and the cheapest way to keep a license-depth run small. Empty = everywhere.

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

Prefix match on the licensee's last name. This is also the axis the Actor uses internally to split any slice too big for the portal's CSV export: A–Z summed to 174,561 of 174,569 physicians (100.00%) and 790,805 of 790,875 registered nurses (99.99%), where county sums to 55.3% and the state's own status buckets to 99.78%.

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

Prefix match on the licensee's first name. Combine with lastName to find a specific person whose license number you do not have.

## `businessName` (type: `string`):

Prefix match for licenses held by a FACILITY rather than a person — pharmacies, clinical labs, EMS providers, health care services pools. Measured 2026-08-12: 27,040 pharmacy-facility licenses and 279 health care services pools exist. Those rows have a business name and no first/last name.

## `detailLevel` (type: `string`):

roster = the bulk read: license number, name, profession, city and status for the WHOLE slice in ONE request (13,853 Miami physicians arrived in a single request, verified against the portal's own count). license = also opens each licensee's verification page for address of record, original issue date, expiry date, days-until-expiry, controlled-substance and cannabis-order flags and the full disciplinary / public-complaint history — roughly one request per licensee. profile = additionally pulls the DOH Practitioner Profile for e-mail, specialty board certifications, hospital staff privileges, Medicaid participation and other-state licenses. Florida publishes that profile for physicians, PAs and ARNPs only, so on nurses, dentists and pharmacists it costs nothing extra and simply leaves those fields null. Measured on 200 practicing Gainesville physicians: e-mail 88%, hospital privileges 57%, specialty certifications 50%, other-state licenses 50%.

## `licenseStatus` (type: `string`):

Practicing only is the default because Florida publishes an address of record ONLY for practicing licenses. Measured on a full 300-licensee run: 100% address fill on the 183 practicing rows versus 9% on the 117 non-practicing ones. ACT = Clear/Active, Obligations/Active, Conditional/Active, Probation/Active, Military Active. PREV = Null and Void, Retired, Deceased, Revoked, Vol Relinquish, Delinquent. Choose All statuses when you are credentialing and need to see the bad ones. Note the state's own two buckets do not quite sum to All — four statuses, Clear/Inactive among them, sit in neither (ACT 91,034 + PREV 83,150 = 174,184 of 174,569). A license-number lookup always searches all statuses regardless of this setting.

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

Hard cap on rows returned AND on what you are billed, at $0.003 per licensee: 100 rows = $0.30, 1,000 = $3.00, 10,000 = $30.00. Duplicates are dropped before billing and never count against it. Roster depth reaches 100,000+ rows comfortably; license and profile depth cost about one page fetch per licensee, so a run in the low thousands is the sensible ceiling there. Ships at 100 so the first click is cheap — which also means a pasted list of more than 100 license numbers stops at 100 until you raise this.

## `proxyConfiguration` (type: `object`):

Leave OFF. The MQA portal is public and unwalled, and proxying it only adds a failure mode. Measured ladder, 18 identical calls per rung: direct 18/18 = 100% in 28 s · Apify automatic proxy 16/18 = 88.9% in 298 s (the two failures were "proxy responded with 590 UPSTREAM502") · Apify residential US with a pinned session 18/18 = 100% in 98 s. Switch it on only if you need a fixed egress IP for your own network policy.

## Actor input object example

```json
{
  "licenseNumbers": [],
  "licenseNumber": "",
  "board": "15",
  "profession": "1501",
  "professionCustom": "",
  "county": "13",
  "city": "",
  "zipCode": "",
  "lastName": "",
  "firstName": "",
  "businessName": "",
  "detailLevel": "license",
  "licenseStatus": "ACT",
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per licensee: license number, name, profession, status, address of record, issue and expiry dates, and any disciplinary actions on file.

# 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 = {
    "licenseNumbers": [],
    "board": "15",
    "profession": "1501",
    "county": "13",
    "detailLevel": "license",
    "licenseStatus": "ACT",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/florida-medical-license-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 = {
    "licenseNumbers": [],
    "board": "15",
    "profession": "1501",
    "county": "13",
    "detailLevel": "license",
    "licenseStatus": "ACT",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/florida-medical-license-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 '{
  "licenseNumbers": [],
  "board": "15",
  "profession": "1501",
  "county": "13",
  "detailLevel": "license",
  "licenseStatus": "ACT",
  "maxItems": 100
}' |
apify call scrapersdelight/florida-medical-license-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/florida-medical-license-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/gqhz3aYSTpoLJjXJr/builds/FlrzPzOTMEFdergFp/openapi.json
