# Licensed Professional Finder — State License Lookup & Contacts (`inovaflow/licensed-professional-finder`) Actor

Find licensed professionals by profession, state and city from official license boards: all US healthcare providers (NPI registry), CA/FL/TX attorneys, Texas real-estate and trade licenses, Florida insurance agents. License status, address, phone, website, e-mails. No login, dataset-only, MCP-ready.

- **URL**: https://apify.com/inovaflow/licensed-professional-finder.md
- **Developed by:** [inovaflow](https://apify.com/inovaflow) (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

from $20.00 / 1,000 licensees

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Licensed Professional Finder — State License Lookup & Contacts

Find **licensed professionals by profession, state and city** straight from the official license boards and
registries, one flat row per licensee: name, license number, type and status, issue/expiry dates, business
name, address, phone, the board record URL — plus website, e-mails and social profiles found for each one.
No login, no API key, dataset-only, MCP-ready.

### Who it is for

- **Vertical SaaS, fintech, insurance and lending teams** selling to regulated professionals — dentists,
  physicians, attorneys, real-estate agents, contractors, insurance producers — who need a list that starts
  from *who is actually licensed*, not from a directory of ads.
- **Recruiters and staffing agencies** building candidate lists of licensed nurses, therapists, pharmacists,
  electricians or adjusters in a metro.
- **Compliance, KYC and onboarding teams** verifying a list of names against the boards (`names` input).
- **AI agents and GTM automations** that need a clean `profession × state × city → licensees` tool.

### What it does

1. You pick a **profession** and one or more **states**, and narrow by **city**, **ZIP**, **counties** — or
   paste **names** to verify a list.

2. The Actor queries every board that publishes that profession in each state, in parallel:

   | Source | Professions | States |
   | --- | --- | --- |
   | **NPPES NPI Registry** (CMS) | physicians, dentists, nurses, nurse practitioners, PAs, pharmacists & pharmacies, chiropractors, PTs, OTs, optometrists, podiatrists, psychologists, counselors, social workers, dietitians, SLPs, acupuncturists, veterinarians, home-health agencies, any other NPPES taxonomy via `specialty` | every US state |
   | **State Bar of California** | lawyers | CA |
   | **The Florida Bar** | lawyers | FL |
   | **State Bar of Texas** | lawyers | TX |
   | **Texas Real Estate Commission** (TREC) | real-estate sales agents, brokers, home inspectors, appraisers | TX |
   | **Texas Dept. of Licensing & Regulation** (TDLR) | A/C & electrical contractors, electricians, barbers, estheticians, tow operators, property-tax consultants, water-well drillers, auctioneers | TX |
   | **Florida Dept. of Financial Services** | insurance agents & agencies | FL |

3. Every licensee is deduplicated (board + state + license number; NPI for the registry), filtered by status,
   optionally enriched with **website, e-mails and social profiles** (board-published contacts first; then a
   name-matched web search for the practice/firm website, a crawl of its contact pages, and search-result
   mining for attributed e-mails), and pushed as one row. A CSV copy and a run summary land in the key-value
   store.

### Why this one

- **Official sources only.** Every row comes from the board or registry that issued the license, with the
  record URL on the row. Nothing is inferred: a field is what the board shows, or `null`.
- **One input for many professions.** Healthcare covers all 50 states out of the box; lawyers, real-estate,
  trades and insurance are added board by board (CA, FL, TX today — the run summary tells you when a state has
  no board source for a profession yet).
- **Contacts you can use.** Board-published e-mails (Florida insurance agents, California and Florida
  attorneys) plus enrichment that only accepts a website whose domain carries the licensee's name and only
  keeps e-mails attributed to that website or name.
- **Pay per row, contacts only when found.** $0.02 per licensee, $0.02 more only when an e-mail or website is
  on the row. Duplicates, filtered rows and empty runs are free.

### Fields

| Field | Description |
| --- | --- |
| `id` | Stable key: `npi:<NPI>` or `<source>:<state>:<licenseNumber>` |
| `fullName`, `firstName`, `lastName` | The licensee (organisations keep the organisation name in `fullName`, `isOrganization: true`) |
| `profession` | Your profession label (e.g. `Dentist`, `Lawyer`, `Insurance agent`) |
| `specialty` | NPPES taxonomy, TREC sub-type, TX Bar practice areas, FL DFS license classes, TDLR designation |
| `licenseNumber`, `licenseType`, `licenseStatus` | As published by the board; NPI rows carry the state license number when NPPES has it (else the NPI) |
| `issuedAt`, `expiresAt` | ISO dates where published (NPI: enumeration date; CA/TX bars: admission/license date; TREC/TDLR: expiration; FL DFS: earliest license issue) |
| `state`, `city`, `county`, `zip`, `address` | Practice/business location as published (TDLR: county only) |
| `phone`, `fax` | Board-published numbers |
| `businessName` | Practice, firm, organisation or DBA |
| `website`, `domain`, `emails[]`, `primaryEmail`, `socials{}` | Board-published contacts + enrichment |
| `contactStatus` | `board` (contact published by the board), `enriched` (found by enrichment), `none` |
| `enrichment` | `{ status, website, emails, socials, pagesCrawled, searchQueries }` or `null` when off |
| `npi` | National Provider Identifier (healthcare rows) |
| `boardUrl` | The licensee's record on the board / registry |
| `source`, `sourceName`, `query`, `scrapedAt` | Provenance |

Two dataset views: **Licensees** (license table) and **Leads & contacts** (business, address, phone, website,
e-mails).

### How to use

1. Choose a `profession` and `states`. Add a `city` (required for the bar directories and FL DFS), a `zip`,
   or `counties` (Texas trade licenses).
2. Keep `licenseStatus: active` for a sales list; switch to `any` for verification work.
3. Leave `enrichContacts` on to get websites and e-mails; turn it off for a fast license list.
4. Run. Read `OUTPUT` in the key-value store for counts per source and any notice (e.g. a state without a
   board for the profession), download `LICENSEES.csv` or read the dataset.

Verify a list: put names in `names` (`"John Smith"` or `"Smith, John"`) with the profession and states; each
name is looked up on every relevant board and a city narrows the match.

### Cost

$0.02 per licensee, +$0.02 when the row has an e-mail or website, $0.005 Actor start. 100 Austin dentists with
enrichment ≈ $2.5–3.5; 500 Florida insurance agents (board e-mails on every row) ≈ $20; a 1,000-row license
list without enrichment ≈ $20. Compute is a few cents per hundred rows (pure HTTP, 1 GB).

### Input

```json
{
    "profession": "dentist",
    "states": ["TX"],
    "city": "Austin",
    "licenseStatus": "active",
    "maxResults": 100,
    "enrichContacts": true
}
```

Other examples: `{ "profession": "lawyer", "states": ["CA"], "city": "Fresno" }` ·
`{ "profession": "insurance-agent", "states": ["FL"], "city": "Tampa", "specialty": "HEALTH" }` ·
`{ "profession": "electrician", "states": ["TX"], "counties": ["Travis", "Williamson"] }` ·
`{ "profession": "realtor", "states": ["TX"], "names": ["Nathan Smith"] }` ·
`{ "profession": "healthcare-other", "specialty": "Midwife", "states": ["CO", "UT"] }`.

### Output sample

```json
{
    "id": "calbar:CA:328226",
    "fullName": "Rosemary C. Gomez",
    "profession": "Lawyer",
    "licenseNumber": "328226",
    "licenseType": "California State Bar license",
    "licenseStatus": "Active",
    "issuedAt": "2019-12",
    "state": "CA",
    "businessName": "California Immigration Project",
    "address": "132 W Nees Ave, 106 2080",
    "city": "Fresno",
    "zip": "93711-6195",
    "website": "http://www.calimm.org",
    "domain": "calimm.org",
    "emails": ["rgomez@calimm.org"],
    "primaryEmail": "rgomez@calimm.org",
    "contactStatus": "board",
    "boardUrl": "https://apps.calbar.ca.gov/attorney/Licensee/Detail/328226",
    "source": "calbar",
    "sourceName": "State Bar of California"
}
```

### FAQ

**Which states are covered?** Healthcare professions: all US states and territories (NPI registry). Lawyers:
CA, FL, TX. Real estate and trade licenses: TX. Insurance: FL. Ask for a state and the run summary says
whether a board source exists for it; boards are added as they prove readable without a login or captcha.

**Why is `city` null on Texas trade rows?** TDLR's public license files carry the county only (verified: no
street address, city or phone in any row). Rows are filtered by county; a `city` you give is resolved to
its county through the board's own search. Enrichment adds the website/e-mail.

**Why does a Texas real-estate `city` query return so few rows?** TREC's public index has no city field (a few
hundred of 140,000 active agents carry one). A `city` therefore matches only those; for a full list drop `city`
(state-wide, newest first — 250 per request) or pass `names`. Street address and ZIP come from each licensee's
detail record.

**Why is a physician's row missing an expiry date?** NPPES has no expiry; the NPI is permanent. `issuedAt` is
the NPI enumeration date, `licenseNumber` the state license number NPPES has on file for the primary taxonomy.

**How many rows can a query return?** NPPES caps one query at 1,200 rows; the Actor splits by ZIP code when a
city exceeds that. The CA Bar shows 500 per query; the Actor splits by last-name initial. Bars and FL DFS page
through every result.

**Are e-mails guessed?** No. E-mails are board-published or found on the licensee's website / attributed
search results with the domain or name check; nothing is pattern-guessed.

**Can I run it from an AI agent / MCP?** Yes — flat rows, stable ids, a `query` label and `boardUrl` per row,
and `OUTPUT` with counts and notices. Use `maxResults` to bound cost.

# Actor input Schema

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

Which licensed profession to list. Healthcare professions are served for every US state (NPI registry); lawyer = CA, FL, TX; realtor / real-estate broker / home inspector / appraiser / contractor / electrician / barber / esthetician / tow operator / property-tax consultant / water-well driller / auctioneer = TX; insurance agent = FL.

## `states` (type: `array`):

US states to search (two-letter codes or names). Each state is queried on every board that covers the profession there; states without a board for that profession are reported in the run summary, never guessed.

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

Narrow to one city. Required for the bar directories and FL DFS (they are not listable state-wide); optional for the NPI registry (practice-location city). TDLR publishes county only, so a city is resolved to its county there; TREC does not index the city at all (only the few records that carry one match — drop "city" for a state-wide Texas list or use "names").

## `zip` (type: `string`):

Narrow to a ZIP code (5 digits) or a ZIP prefix (3–4 digits, NPI registry only). Takes precedence over the city for the NPI registry.

## `counties` (type: `array`):

County names for TDLR trade licenses (e.g. \["Travis", "Williamson"]). TDLR's public files carry no street address or city, only the county.

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

Optional refinement. NPI: an NPPES taxonomy description (e.g. "Orthodontics", "Family Medicine", "Midwife") — replaces the profession's default taxonomies. TREC: a license sub-type (Salesperson, Broker Individual, Broker Company, Professional, Certified General …). FL DFS: a word of the license class to keep (e.g. "HEALTH", "LIFE", "PROPERTY").

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

"active" keeps only licensees the board shows as active / eligible / not expired; "any" returns every status.

## `names` (type: `array`):

Verify a list instead of listing an area: one person per line ("John Smith" or "Smith, John"). Each name is looked up on every board for the profession and states given; a city narrows the match.

## `maxResults` (type: `integer`):

Stop after this many licensees (all sources combined).

## `enrichContacts` (type: `boolean`):

For every licensee: find the practice/firm website (from the board or a name-matched web search), crawl it for e-mails and social profiles, and mine search results for attributed e-mails. Adds 5–30 s per licensee; the `contact` event is charged only when an e-mail or website was found.

## `onlyWithEmail` (type: `boolean`):

Deliver (and charge) only licensees with at least one e-mail address.

## `fetchDetails` (type: `boolean`):

Read each licensee's detail page where the board's list lacks address, phone or dates (CA/FL/TX bars, FL DFS, TREC). Off = list fields only, faster.

## `includeOrganizations` (type: `boolean`):

Keep organisation records (NPI-2 group practices, broker companies, LLCs) alongside individuals.

## `maxSearchQueries` (type: `integer`):

Search-engine queries used for website discovery and e-mail mining per licensee (0 = website crawl only).

## `maxPagesPerSite` (type: `integer`):

Pages crawled per licensee website when enrichment is on (home + contact-like pages).

## `maxConcurrency` (type: `integer`):

Parallel detail fetches and website crawls.

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

Proxy for the license boards and search engines. Every board answered Apify datacenter proxies during development; switch to residential if a board starts challenging.

## Actor input object example

```json
{
  "profession": "dentist",
  "states": [
    "TX"
  ],
  "city": "Austin",
  "licenseStatus": "active",
  "maxResults": 20,
  "enrichContacts": true,
  "onlyWithEmail": false,
  "fetchDetails": true,
  "includeOrganizations": true,
  "maxSearchQueries": 2,
  "maxPagesPerSite": 3,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `licensees` (type: `string`):

One row per licensee: name, profession, license number/type/status, dates, business, address, phone, website, e-mails, board record URL.

## `leads` (type: `string`):

The same rows in the lead layout: business, address, phone, website, e-mails, socials.

## `csv` (type: `string`):

Spreadsheet / CRM-ready CSV of the rows (first 5,000).

## `summary` (type: `string`):

Counts per source and state, rows with e-mail, duplicates, filtered rows, notices about boards that could not serve the query.

# 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 = {
    "profession": "dentist",
    "states": [
        "TX"
    ],
    "city": "Austin",
    "maxResults": 20,
    "enrichContacts": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("inovaflow/licensed-professional-finder").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 = {
    "profession": "dentist",
    "states": ["TX"],
    "city": "Austin",
    "maxResults": 20,
    "enrichContacts": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("inovaflow/licensed-professional-finder").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 '{
  "profession": "dentist",
  "states": [
    "TX"
  ],
  "city": "Austin",
  "maxResults": 20,
  "enrichContacts": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call inovaflow/licensed-professional-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,inovaflow/licensed-professional-finder"
        }
    }
}
```

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/40UKaRYZedQnGPMG4/builds/n2iWMCs64jIdKpYee/openapi.json
