# Illinois Professional Licenses - IDFPR Roster & Discipline (`j0401/il-professional-licenses`) Actor

Illinois IDFPR's full professional licence register (public open data, 4.2M rows, 63 boards): licensee name, business, status and dates, plus the disciplinary case, action and written reason behind 103K disciplined licences.

- **URL**: https://apify.com/j0401/il-professional-licenses.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.06 / 1,000 il professional licence records

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

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

## What's an Apify Actor?

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

## Illinois Professional Licenses - IDFPR Roster & Discipline

### Low cost

**From $0.0001 per record, down to $0.00006 at Gold** - pay per record delivered, and nothing for the query. Cost scales with what you pull, not with the size of the register.

Illinois' full professional and occupational licence register, from IDFPR. **~4.16M rows over ~3.88M distinct licences** across 63 licence types - nursing, medicine, real estate, cosmetology, pharmacy, engineering, dentistry and the rest of the state's licensed trades.

### What you get

Every licensee with their name, business and DBA, licence status, and the issue, effective and expiration dates, plus city, county and ZIP.

And the part most rosters leave out: **the discipline record.** ~102K licences carry a disciplinary flag, ~92K have a case number, ~92K a discipline start date and ~80K the written reason - the action taken (revocation, suspension, probation, fine, reprimand, refuse-to-renew) and, where the state recorded one, the free-text finding explaining it.

### Modes

- **rows** (default) - licence records matching your filters
- **aggregate** - counts by board, status, county or state

### Filters

Licence number, name (first or last), board, licence status, specialty qualifier, business name, DBA, city, county, state, ZIP, business-vs-person, ever-disciplined, and discipline case number.

Expiry, issue and last-modified year are filterable by a 4-digit year. The source stores every date as `MM/DD/YYYY` text and offers no way to compare them as dates, so year-granularity is the honest boundary - and every date is normalized to `YYYY-MM-DD` on output so you can do exact work on the results.

### Example inputs

**One licence by number** - `licenseNumber` is an exact match.

```json
{ "licenseNumber": "DOPO998927" }
```

**Active nurses in Chicago** - `licenseType` is a board-name substring.

```json
{
  "licenseType": "NURSING",
  "licenseStatus": "ACTIVE",
  "city": "CHICAGO",
  "maxResults": 25
}
```

**Licences the state has disciplined** - `everDisciplined=Y` selects licences with at least one case on record.

```json
{ "everDisciplined": "Y", "maxResults": 25 }
```

**The roster by board** - `mode=aggregate` returns one count row per group.

```json
{ "mode": "aggregate", "groupBy": "licenseType" }
```

### Why this is hard

The status vocabulary has 31 exact values, and several are a base status plus a `CHAPERONE REQUIRED` suffix - `ACTIVE` and `ACTIVE CHAPERONE REQUIRED` are different statuses, as are the `SUSPENDED` pair. A substring match on "active" quietly returns both and you would never know.

Row count is not licence count, for two separate reasons. **~224K rows carry a masked licence number** - the state blanks the digits (`33*****04`), and those masks collapse onto only **800 distinct strings**, so roughly 224K real licences share 800 identifiers. A further ~38K rows have no number at all. The remaining ~23K duplicates are the genuine ones: a licence with several disciplinary actions appears once per action. So a licence number is a search key, not a primary key - dedupe on your side if you need one row per licence.

\~90K records carry an expiration date of 2099 - the state's way of writing "no expiration on file", not a real expiry ninety years out.

### Output

Dates normalized to `YYYY-MM-DD`; the same key set on every record.

### Example output

**One licence** - `licenseNumber=DOPO998927` returns the licence record:

```json
{
 "platform": "il-licenses",
 "source": "il-idfpr-licenses",
 "corpus": "licenses",
 "recordKind": "license",
 "mode": "rows",
 "groupKey": "",
 "groupCount": "",
 "groupBy": "",
 "licenseType": "MEDICAL CANNABIS BD",
 "description": "REGISTERED PRINCIPAL OFFICER",
 "licenseNumber": "DOPO998927",
 "licenseStatus": "TERMINATED",
 "business": "N",
 "title": "",
 "firstName": "ROBERT",
 "middleName": "O'S.",
 "lastName": "REMIEN",
 "prefix": "",
 "suffix": "",
 "businessName": "ROBERT O'S. REMIEN",
 "businessDba": "",
 "originalIssueDate": "2018-09-19",
 "effectiveDate": "2024-09-19",
 "expirationDate": "2025-09-19",
 "city": "PLEASANT PRAIRIE",
 "state": "WI",
 "zip": "53158",
 "county": "",
 "specialtyQualifier": "",
 "controlledSubstanceSchedule": "",
 "delegatedControlledSubstance": "",
 "everDisciplined": "N",
 "lastModified": "2025-09-10",
 "caseNumber": "",
 "action": "",
 "disciplineStartDate": "",
 "disciplineEndDate": "",
 "disciplineReason": "",
 "sourceUpdatedAt": "2026-09-21"
}
```

**`mode=aggregate`, `groupBy=licenseType`** - one count row per board:

```
DETECTIVE BOARD        752,104
NURSING BOARD          682,269
REAL ESTATE            637,755
COSMO                  403,598
MEDICAL BOARD          387,679
PHARMACY               366,027
```

### Notes

- Public open data from data.illinois.gov. No login, no scraping.
- IDFPR publishes the roster and the discipline record as part of its licensing duty.
- Charges are metered per record delivered, so a targeted query costs a fraction of a cent.

# Actor input Schema

## `mode` (type: `string`):

rows = licence records matching your filters (default). aggregate = one count row per group.

## `groupBy` (type: `string`):

Aggregate dimension, only used when mode=aggregate.

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

Exact licence number, e.g. '198000121'. Blank = any. Note the source masks some numbers (e.g. '33\*\*\*\*\*04') and leaves 38,555 blank.

## `name` (type: `string`):

Substring matching the licensee's last OR first name, e.g. 'SMITH'. Blank = any.

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

First-name substring. Blank = any.

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

Last-name substring. Blank = any.

## `licenseType` (type: `string`):

Board-name substring, e.g. 'NURSING', 'REAL ESTATE', 'COSMO'. Blank = any. 63 boards in total.

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

Exact licence status. Blank = any. Some values are a base status plus a CHAPERONE REQUIRED suffix - they are distinct statuses.

## `specialtyQualifier` (type: `string`):

Specialty-qualifier substring (e.g. a nursing or dental specialty). Blank = any.

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

Business-name substring, e.g. a hospital or agency. Blank = any.

## `dba` (type: `string`):

Business DBA substring. Blank = any.

## `business` (type: `string`):

Y = a business/entity licence; N = a natural person. Blank = both.

## `everDisciplined` (type: `string`):

Y = this licence has at least one disciplinary case on record. Blank = both. Pair with licenseNumber to pull a full discipline history.

## `caseNumber` (type: `string`):

Exact disciplinary case number. Blank = any.

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

City substring, e.g. 'CHICAGO'. Blank = any.

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

County substring, e.g. 'COOK'. Blank = any.

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

Two-letter state, e.g. 'IL'. Blank = any.

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

ZIP prefix, e.g. '60615' or '606'. Blank = any.

## `expirationYear` (type: `string`):

4-digit year: matches licences EXPIRING in that year, e.g. '2027'. Blank = any. The source stores dates as MM/DD/YYYY text, so filtering is year-granularity only. 2099 is the source's no-expiration sentinel and is rejected.

## `issuedYear` (type: `string`):

4-digit year: matches licences originally ISSUED in that year. Blank = any.

## `modifiedYear` (type: `string`):

4-digit year: matches records last MODIFIED in that year - use it to pull a recent-change slice. Blank = any.

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

Cap records returned for this run. Default 50 keeps the daily auto-test fast. 0 = no cap (the full matching slice, up to 10,000).

## Actor input object example

```json
{
  "mode": "rows",
  "groupBy": "licenseType",
  "licenseNumber": "",
  "name": "",
  "firstName": "",
  "lastName": "",
  "licenseType": "",
  "licenseStatus": "",
  "specialtyQualifier": "",
  "businessName": "",
  "dba": "",
  "business": "",
  "everDisciplined": "",
  "caseNumber": "",
  "city": "",
  "county": "",
  "state": "",
  "zip": "",
  "expirationYear": "",
  "issuedYear": "",
  "modifiedYear": "",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

Illinois professional licence records - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/il-professional-licenses").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("j0401/il-professional-licenses").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 '{}' |
apify call j0401/il-professional-licenses --silent --output-dataset

```

## MCP server setup

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

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/DHldUuA4ofkVTqZ4w/builds/xzkEFKddAAcfhnZjI/openapi.json
