# Ohio Contractor Licenses - OCILB Electrical, HVAC, Plumbing (`j0401/oh-contractors`) Actor

Ohio OCILB contractor licenses (12,500+ licensed commercial contractors, free public data): electrical, HVAC, hydronics, plumbing, refrigeration and training agencies - license number, holder, company, city, county, status and expiration. Search by name, company, city or license number.

- **URL**: https://apify.com/j0401/oh-contractors.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.03 / 1,000 ohio contractor license 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

## Ohio Contractor Licenses - OCILB Commercial Contractor Register

The Ohio Construction Industry Licensing Board (**OCILB**) licenses every commercial contractor who installs, services or trains on electrical, HVAC, hydronics, plumbing and refrigeration work in the state - and publishes the register as **free public data**. This actor turns that register into a **charged-per-record lookup and screening tool**.

**Built for:** contractor vetting and compliance - verifying that an electrician, HVAC or plumbing contractor is actually licensed and in good standing, checking whether a license is active or in renewal, finding every licensed contractor in a county or city, matching a company name to its license, and prospecting the trades by trade, city and expiration.

### What it covers

**12,523 licensed contractors** across six OCILB license classes:

| License class | Contractors |
|---|---|
| Electrical contractor (EL) | 4,450 |
| HVAC contractor (HV) | 3,103 |
| Plumbing contractor (PL) | 3,040 |
| Hydronics contractor (HY) | 941 |
| Refrigeration contractor (RE) | 811 |
| Training agency (TA) | 178 |

Every row carries the **license number** (`EL.10514`, `PL.10586`, `TA.00001` ), the **license class**, the **holder's name**, the **status** (ACTIVE / ACTIVE IN RENEWAL), the **effective and expiration dates**, the **county** and **state** of the holder, and the **contracting company** with its address, city, state, ZIP and phone.

> **Two things worth knowing.** Status is binary in the register - `ACTIVE` (10,255) or `ACTIVE IN RENEWAL` (2,268, i.e. a renewal is pending but the license has not lapsed). And the register is statewide but not Ohio-only: licensed holders live in **Ohio 10,542**, then Kentucky 406, Indiana 219, Michigan 185, Pennsylvania 154, West Virginia 140 - contractors who carry an Ohio license from out of state.

### Typical questions

- "Is **this electrician's license** (`EL.10514`) active, and when does it expire?"
- "Show every **licensed plumbing contractor in Cuyahoga County**."
- "Which companies with **'ELECTRIC'** in the name hold an Ohio contractor license?"
- "Find **HVAC contractors in Cleveland**."
- "Which licenses **expire in 2027**?"
- "How are Ohio contractor licenses **distributed by class, city and county**?"

### Inputs

| Input | What it does |
|---|---|
| `credential` | exact license number -> that contractor |
| `name` | holder name substring, case-insensitive |
| `company` | company name substring, case-insensitive |
| `city` / `county` / `state` / `zip` | business city / holder county / holder state / company ZIP prefix |
| `licenseType` | license class - the code (`EL`, `HV`, `HY`, `PL`, `RE`, `TA`) or the name (`ELECTRICAL`, `PLUMBING` ) |
| `status` | exact status (`ACTIVE` or `ACTIVE IN RENEWAL`) |
| `expireFrom` / `expireTo` | expiration date window (accepts `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or `M/D/YYYY`) |
| `aggregate` / `groupBy` | one summary row per group with a count - class / city / county / status |
| `maxResults` | cap records (default 200). The `aggregate` mode returns every group in full and is **not** cut by this cap. |

**Default run = a tiny aggregate** (the license-class mix) - fast and safe for the daily auto-test, never a surprise full dump. For records, add a `county` / `company` / `city` filter or a `credential`.

### Example inputs

**Look up one license** - a bare license number returns that contractor.

```json
{ "credential": "EL.10514" }
```

**Electrical contractors in Cuyahoga County** -

```json
{ "licenseType": "EL", "county": "Cuyahoga", "maxResults": 10 }
```

**Every company with "ELECTRIC" in its name** -

```json
{ "company": "ELECTRIC", "maxResults": 10 }
```

**Licenses expiring in 2027** -

```json
{ "expireFrom": "2027-01", "expireTo": "2027-12", "maxResults": 10 }
```

**The license-class mix** - one summary row per class, statewide.

```json
{ "aggregate": true, "groupBy": "licenseType", "maxResults": 10 }
```

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold** - billed only for the rows you use, and each record is metered individually.

OCILB's register is a whole-register roster behind a form on the state's licensing portal - there is **no query API and no way to ask it for "just the licensed electricians in Cuyahoga County"**. Rather than drive that form on every lookup, the register is **mirrored**, and each run queries the mirror in a single pass. That mirror is what lets a 12,500-contractor register behave like a live lookup at per-record pricing. Every **refresh** is integrity-checked against the register's known shape (total count, the presence of all six license classes, key-column coverage), so a degraded or schema-changed source **fails loudly at refresh** instead of publishing bad rows.

### Example output

**`credential=EL.10514`** - a licensed electrical contractor:

```json
{
 "platform": "oh-contractors",
 "source": "oh-ocilb-contractors",
 "state": "OH",
 "credential": "EL.10514",
 "licenseType": "EL",
 "licenseTypeName": "ELECTRICAL CONTRACTOR",
 "status": "ACTIVE IN RENEWAL",
 "name": "ARTHUR V DELOY JR",
 "lastName": "DELOY",
 "county": "TRUMBULL",
 "holderState": "OH",
 "effectiveDate": "2026-01-01",
 "expirationDate": "2026-12-31",
 "company": "DC GENERATORS LLC",
 "companyAddress": "522 JOHNSON PLANK ROAD NE",
 "companyAddress2": "",
 "companyCity": "WARREN",
 "companyState": "OH",
 "companyZip": "44481",
 "companyPhone": "(330) 847-6777",
 "companyFax": "",
 "companyEmail": "",
 "refreshed": "2026-10-13",
 "mode": "record",
 "groupKey": "",
 "groupCount": "",
 "groupBy": ""
}
```

**`aggregate=true`, `groupBy=companyCity`** - contractors by city (top):

```
CINCINNATI       663
COLUMBUS         516
CLEVELAND        372
(blank)          299
DAYTON           285
TOLEDO           208
```

**`aggregate=true`, `groupBy=county`** - by county (top):

```
CUYAHOGA       1,045
FRANKLIN         725
HAMILTON         616
MONTGOMERY       435
SUMMIT           411
```

### Source

- [Ohio eLicense - OCILB Roster Lookup](https://elicense4.com.ohio.gov/Lookup/GenerateRoster.aspx) - the official free public register this actor reads.
- Data reflects the Board's record as of the refresh; the city and county fields are normalised to upper-case (the source mixes casing).

# Actor input Schema

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

lookup = one license number. aggregate = one row per group. Leave blank to auto-route: a bare credential -> lookup, otherwise search (or aggregate when aggregate=true).

## `credential` (type: `string`):

Exact OCILB license number, e.g. "EL.10514" (Electrical), "PL.10586" (Plumbing), "TA.00001" (Training Agency). Returns that contractor's record.

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

License holder name substring, case-insensitive (e.g. "SMITH").

## `company` (type: `string`):

Contracting company name substring, case-insensitive (e.g. "ELECTRIC").

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

Company city substring, case-insensitive (e.g. "CLEVELAND").

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

License holder's county substring, case-insensitive (e.g. "Cuyahoga").

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

License holder's state, exact 2-letter code (e.g. "OH", "KY").

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

Company ZIP prefix, digits only (e.g. "441").

## `status` (type: `string`):

License status, exact match.

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

License class: the 2-letter code (EL, HV, HY, PL, RE, TA) or the class name (e.g. "ELECTRICAL", "PLUMBING").

## `expireFrom` (type: `string`):

Earliest expiration date (inclusive). Accepts "YYYY", "YYYY-MM", "YYYY-MM-DD" or "M/D/YYYY".

## `expireTo` (type: `string`):

Latest expiration date (inclusive). Accepts "YYYY", "YYYY-MM", "YYYY-MM-DD" or "M/D/YYYY".

## `aggregate` (type: `boolean`):

When on, returns one summary record per group (with a count) instead of individual licenses.

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

licenseType -> the class mix; companyCity -> city distribution; county -> county distribution; status -> active vs in-renewal.

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

Cap the number of records pushed (0 = a default of 200). The aggregate mode returns every group in full and is not cut by this cap.

## Actor input object example

```json
{
  "aggregate": false,
  "groupBy": "licenseType",
  "maxResults": 200
}
```

# Actor output Schema

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

Ohio OCILB contractor license 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/oh-contractors").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/oh-contractors").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/oh-contractors --silent --output-dataset

```

## MCP server setup

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

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/FBXJVAAnch1kz630i/builds/OCXgidxN0LfGtdzaG/openapi.json
