Florida Medical License Scraper — DOH MQA Verification avatar

Florida Medical License Scraper — DOH MQA Verification

Pricing

$3.00 / 1,000 per licensee record returneds

Go to Apify Store
Florida Medical License Scraper — DOH MQA Verification

Florida Medical License Scraper — DOH MQA Verification

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.

Pricing

$3.00 / 1,000 per licensee record returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

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 — 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.

{
"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)

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)

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

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

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:

"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:

rowsaddressZIPexpiry date
Practicing (Clear/Active etc.)183100%97%100%
Non-practicing (Null & Void, Deceased, Retired, Revoked…)1179%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

FieldTypeNotes
licenseNumberstringe.g. ME58861, RN9268431. The dedupe key, with professionCode.
namestringExactly as DOH printed it. LAST, FIRST MIDDLE on search runs, display order on license-number lookups.
lastName / firstNamestring | nullSplit only when DOH supplies the comma form — see gotcha 4.
professionstringe.g. Medical Doctor, Registered Nurse.
professionCodestringe.g. 1501, 1701.
board / boardCodestring | nullPopulated when you filtered by board.

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

FieldTypeNotes
licenseStatusstringClear/Active, Null And Void, Retired, Revoked, Probation/Active, Military Active, Obligations/Active, Conditional/Active, Clear/Inactive, Vol Relinquish, Delinquent, Deceased
isPracticingbooleanTrue for the five statuses the portal itself counts as practicing.
licenseOriginalIssueDatedate
licenseExpirationDatedate
daysUntilExpiryintegerNegative when already expired. Computed at scrape time.

Address of record

FieldTypeNotes
addressLine1 / addressLine2string | null
city / state / zipstring | null
countystring | nullEchoed from your filter; DOH never prints it.
addressWithheldbooleantrue = DOH published a "contact the Department" notice instead of an address.

Regulatory

FieldTypeNotes
disciplineOnFile / publicComplaintbooleanThe portal's own flags.
disciplinaryActionsarray | null{type, name, license, profession, city, state, caseNumber, actionTaken, documentId}
controlledSubstancePrescriberbooleanChronic non-malignant pain prescriber flag.
authorizedToOrderCannabisboolean | nullNull means the portal did not render the row at all — see limits.
qualificationsstring | nulle.g. Dispensing Practitioner.

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

FieldTypeNotes
emailstring | nullThe address the practitioner published for contact.
specialtyCertificationsarray | null{specialtyBoard, certification, dateCertified}
hospitalPrivilegesarray | null{institution, city, state}
otherStateLicensesarray | null{state, profession}
medicaidParticipantboolean | null
practiceAddressstring | null
yearBeganPracticingstring | nullAs DOH prints it on the profile — usually a full date (07/01/1996), sometimes blank.
profileAvailablebooleanWhether DOH publishes a profile for this licensee at all.

Provenance

FieldTypeNotes
licenseVerificationUrlstringThe exact DOH page this row came from — put it in the credentialing file.
sourceUrl / scrapedAtstringISO-8601 UTC timestamp.
detailLevelstringThe 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)

FieldFill
licenseNumber, name, lastName, firstName, profession, professionCode, board, boardCode, county100%
licenseStatus, isPracticing, licenseExpirationDate, licenseOriginalIssueDate, daysUntilExpiry100%
city100%
disciplineOnFile, publicComplaint, controlledSubstancePrescriber, addressWithheld, profileAvailable100%
addressLine1, zip, state98%
addressLine232%
qualifications22%
disciplinaryActions7%
authorizedToOrderCannabis6%
email, specialtyCertifications, hospitalPrivileges, otherStateLicenses, medicaidParticipant0% (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)

FieldFill
Identity, status, dates, addressLine1, city, practiceAddress100%
zip, state98%
email88%
medicaidParticipant66%
yearBeganPracticing63% (re-measured 2026-08-19 on 30 Gainesville physicians; 0% before that build — see changelog)
hospitalPrivileges57%
specialtyCertifications50%
otherStateLicenses50%
addressLine248%
qualifications28%
disciplinaryActions2%
authorizedToOrderCannabis0%
county0% (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

detailLevelRequestsWhat you get
roster1 for the whole slicelicense 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

{
"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

{ "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

{ "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

{ "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

{ "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

{
"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

{
"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.

FieldTypeDefaultWhat it does
licenseNumbersstring list[]Batch primary-source verification. One request per number, all statuses. Unmatched numbers are logged, saved to NOT_FOUND_LICENSE_NUMBERS and never charged.
licenseNumbertext""Single lookup — identical to one entry above. The run-sync API path.
boardselect (36)AnyFlorida MQA board or council. Sizes in the table below.
professionselect (237)AnyEach option is labelled with the board that owns it. Unique across boards, so it can be used alone.
professionCustomtext""Escape hatch — a profession name or code not in the dropdown. Overrides it.
countyselect (67)AnyCosts you ~45% of the population — see gotcha 2.
citytext""Exact match on DOH's spelling, e.g. GAINESVILLE.
zipCodetext""5-digit ZIP. Tightest geographic filter, and the cheapest way to keep a deep run small.
lastNametext""Prefix match. Also the axis the Actor splits oversized slices on.
firstNametext""Prefix match.
businessNametext""Prefix match for facility licenses (pharmacies, labs, EMS providers).
detailLevelselectrosterroster · license · profile. Same price at every depth. The form is prefilled with license.
licenseStatusselectACTPracticing only, non-practicing only, or all. Ignored on license-number lookups, which always search all statuses.
maxItemsinteger100Row cap and your billing cap. 0 is not a wildcard — set a number.
proxyConfigurationproxyoffLeave off. Measured ladder below.

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

CodeBoardRecords
17BOARD OF NURSING1,573,814
22BOARD OF PHARMACY272,596
15BOARD OF MEDICINE269,685
25BUREAU OF EMERGENCY MEDICAL SERVICES258,162
76RADIATION CONTROL179,419
14BOARD OF MASSAGE THERAPY152,118
52BOARD OF CSW/MFT/MHC122,492
7BOARD OF DENTISTRY105,285
66BOARD OF CLINICAL LABORATORY PERSONNEL77,360
55BOARD OF PHYSICAL THERAPY PRACTICE62,806
30SPEECH-LANGUAGE PATHOLOGY AND AUDIOLOGY50,979
57BOARD OF RESPIRATORY CARE44,975
96OUT-OF-STATE TELEHEALTH PROVIDERS43,552
56BOARD OF OCCUPATIONAL THERAPY PRACTICE38,928
5BOARD OF CHIROPRACTIC MEDICINE37,149
19BOARD OF OSTEOPATHIC MEDICINE35,258
26EMS PROVIDERS29,739
24PUBLIC SAFETY TELECOMMUNICATIONS22,688
20BOARD OF OPTICIANRY17,563
61DIETETICS AND NUTRITION PRACTICE COUNCIL15,682
27BOARD OF PSYCHOLOGY11,666
18BOARD OF OPTOMETRY10,538
65ELECTROLYSIS COUNCIL8,951
21BOARD OF PODIATRIC MEDICINE8,296
10BOARD OF ATHLETIC TRAINING7,859
8NURSING HOME ADMINISTRATORS5,706
36BOARD OF HEARING AID SPECIALISTS5,549
38BOARD OF ACUPUNCTURE4,750
31BOARD OF ORTHOTISTS AND PROSTHETISTS2,326
41BOARD OF SCHOOL PSYCHOLOGY2,025
60ADVISORY COUNCIL OF MEDICAL PHYSICIST1,981
53GENETIC COUNSELING982
32COUNCIL OF LICENSED MIDWIFERY524
62HEALTH CARE SERVICES POOLS279
16BOARD OF NATUROPATHIC MEDICINE56
54CERTIFIED SOCIAL WORKERS23

Total: 3,481,761.

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

CodeProfessionAll statusesPracticing
1701Registered Nurse790,880395,219
4401Certified Nursing Assistant484,858152,572
1702Licensed Practical Nurse213,47562,469
1501Medical Doctor174,57591,039
2501Emergency Medical Technician161,48450,538
2208Registered Pharmacy Technician137,61559,709
1401Massage Therapist107,51936,723
1711Advanced Practice Registered Nurse82,86469,531
2502Paramedic65,41639,928
2201Pharmacist60,14437,204
5501Physical Therapist41,38721,678
702Dental Hygienist32,90519,225
701Dentist30,27918,638
2205Pharmacy (facility)27,04011,036
5601Occupational Therapist25,48212,996
3001Speech-Language Pathologist24,86813,924
1512Physician Assistant23,83315,673
1901Osteopathic Physician22,89213,122
501Chiropractic Physician14,9647,583
2701Psychologist11,2627,009
1801Optometrist6,6274,247
2101Podiatric Physician4,4181,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.

RunLicenseesCost
The default first click (Bay County physicians)100$0.30
Every practicing physician in Bay County460$1.38
Every practicing physician in Gainesville2,326$6.98
Every practicing physician in Miami-Dade10,903$32.71
Every practicing physician in Florida91,039$273.12
Every practicing APRN in Florida69,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:

StepEndpointPurpose
1GET /HealthCareProvidersSession cookie, __RequestVerificationToken, and the live board / county / status dropdowns
2POST /HealthCareProvidersReturns "Search Results Total: N" — the authoritative count, and binds the session to this result set
3GET /ExportToCsvLVP?jsonModel={…}Stateless bulk export — the whole result set as CSV in one request (174,569 rows / 13.7 MB in 31 s measured)
4GET /IndexPaged?page=N20 rows per page, session-bound; yields the internal LicInd the detail pages are keyed by
5GET /LicenseVerification?LicInd=&ProCde=Address, issue and expiry dates, flags
6GET /LoadEnforcementActionsTab?…Discipline cases, emergency actions, public complaints
7GET /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

RouteSuccessTime
Direct, no proxy18/18 = 100%28 s
Apify automatic proxy16/18 = 88.9%298 s
Apify RESIDENTIAL, US, pinned session18/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:

SliceAuthoritative countRows the export returned
Medical Doctor, statewide174,569174,569 ✅
All of Dentistry105,284105,284 ✅
All of Pharmacy272,5930
Registered Nurse790,8750
All of Nursing1,573,8060

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:

fetcheduniquelost
Sort pinned75874315 licensees
Default order (what this Actor uses)7587580

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:

RunExpectedEmittedDuplicatesVerified coverage
100 practicing Bay County physicians, license depth1001000100.0%
200 practicing Gainesville physicians, profile depth2002000100.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 teamslicenseStatus 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?

ActorWhat it isWhy you would use it instead
This oneFlorida DOH / MQA — health care practitionersDoctors, nurses, dentists, pharmacists, therapists. Discipline file. Primary source.
Florida Contractor License Scraper — DBPRFlorida DBPR — the other Florida licensing agencyContractors, real estate, cosmetology, CPAs. A CGC… or SL… number belongs there, not here.
NPI Registry ScraperThe federal NPPES/NPI registry, all 50 statesYou need providers nationwide, or you need an NPI. MQA does not publish NPIs.
Healthgrades ScraperConsumer directory — doctors and dentistsYou want ratings, reviews and phone numbers. Not a primary source, but it has the phone this one does not.
Wellness.com Provider ScraperConsumer directory — practices and providersPractice-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 SheetsIMPORTDATA 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 ActorManual portal lookupYour own scraperA paid credentialing API
SetupMinutesNoneDays, plus the seven traps aboveContract + onboarding
Bulk extractionYes, whole boardsNo, one at a timeYes, if you solve truncation and paginationUsually per-lookup only
Disciplinary fileStructured, with document IDsManual tab-clickingExtra workSometimes
Cost per 1,000$3.00Staff timeDev + proxy + maintenanceTypically per-seat or per-verification
Primary sourceYes — DOH itselfYesYesOften 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).

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.


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

VersionDateChange
0.2.102026-08-20Cost-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.62026-08-19Address 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.22026-08-12Batch 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.12026-08-12First 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.