# Arizona ROC Contractor License Scraper (`ws_tony/az-roc-license-scraper`) Actor

Scrape the Arizona Registrar of Contractors (AZ ROC) public license database. Search by license number, company name, qualifying party, or city. Returns license status, bond details, complaint history, and full contractor info. $4.00 per 1,000 results.

- **URL**: https://apify.com/ws\_tony/az-roc-license-scraper.md
- **Developed by:** [Tony](https://apify.com/ws_tony) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## 🏗️ Arizona ROC Contractor License Scraper

Scrape the **Arizona Registrar of Contractors (AZ ROC)** public license database with **full pagination support**. Search by license number, company name, qualifying party, or city. Returns complete license details — bond status, complaint history, personnel, and classification codes.

**Pricing: $4.00 / 1,000 results** — pay only for what you collect.

---

### Who uses this

- **General contractors** — verify subcontractor license status and bond coverage before signing a contract
- **Lead generation** — build targeted lists of active contractors by city or trade classification
- **Compliance teams** — bulk-check license numbers against an internal vendor list
- **Homeowners & realtors** — confirm a contractor is properly licensed before paying a deposit
- **Researchers** — analyze complaint patterns, bond requirements, or market density by city

---

### Search modes

Provide any combination of the inputs below. Each entry runs as an independent search job.

| Input | Example | Notes |
|---|---|---|
| `licenseNumbers` | `["333282", "12345"]` | Fastest — direct lookup by AZ ROC number. Produces a `found: false` record for any number not in the database. |
| `companyNames` | `["Desert HVAC", "Acme Plumbing"]` | Partial / fuzzy match — "Arizona Painting" returns all companies with those words in the name |
| `qualifyingPartyNames` | `["John Smith"]` | Searches by individual qualifying party name |
| `cities` | `["Scottsdale", "Mesa"]` | See ⚠️ note below |

> **⚠️ How the AZ ROC portal searches:** The portal uses a **single text-search box** — it does not have separate fields for city, company name, or QP. When you search `cities: ["Tempe"]`, the portal finds all records containing the word "Tempe" anywhere, including company names like "Temperature Control Plumbing." This is Salesforce platform behavior, not a bug. For precise city filtering, use `scrapeDetailPage: true` and filter the output dataset's `city` field after the run.

#### Filters

| Filter | Options | Behavior |
|---|---|---|
| `licenseType` | `ALL / RESIDENTIAL / COMMERCIAL / DUAL` | Applied post-scrape to collected records |
| `licenseStatus` | `ALL / ACTIVE / SUSPENDED / EXPIRED / REVOKED / CANCELLED` | Applied post-scrape — does not narrow the portal search itself |
| `licenseClassification` | Any code, e.g. `B-1`, `CR-42`, `C-37` | Applied post-scrape. See common codes below. |

> **Note on filters:** `licenseStatus` is enforced post-scrape — records collected from the portal are filtered before being pushed to your dataset. `licenseClassification` filters the output to only records matching that code. Neither filter narrows the portal's server-side text search, but both are applied to what reaches your dataset.

#### Common classification codes

| Code | Trade | Type |
|---|---|---|
| `B-1` | General Commercial Contractor | Commercial |
| `A` | General Dual Contractor (residential + commercial) | Dual |
| `CR-11` / `C-11` | Electrical | Residential / Commercial |
| `CR-37` / `C-37` | Plumbing | Residential / Commercial |
| `CR-39` / `C-39` | **Air Conditioning & Refrigeration (HVAC)** | Residential / Commercial |
| `CR-42` / `C-42` | **Roofing** | Residential / Commercial |
| `CR-10` / `C-10` | Drywall | Residential / Commercial |
| `CR-8` / `C-8` | Floor Covering | Residential / Commercial |
| `CR-34` / `C-34` | Painting and Wall Covering | Residential / Commercial |
| `CR-61` / `C-61` | Carpentry, Remodeling and Repairs | Residential / Commercial |
| `CR-16` / `C-16` | Fire Protection Systems | Residential / Commercial |
| `CR-6` / `C-6` | Swimming Pool Service and Repair | Residential / Commercial |
| `CR-60` / `C-60` | Finish Carpentry | Residential / Commercial |

> **Common mistake:** `CR-39` is Air Conditioning, **not** Roofing. Roofing is `CR-42` (residential) or `C-42` (commercial). The `CR-` prefix always means Residential specialty; `C-` means Commercial specialty.

---

### Quick-start examples

#### Batch license number lookup (fastest, cheapest)

```json
{
    "licenseNumbers": ["333282", "333239", "333681"],
    "scrapeDetailPage": false
}
````

Returns immediately with list-level data. Missing license numbers appear as `{ "found": false }` records so you always know which ones weren't found.

#### Verify one contractor before hiring

```json
{
    "licenseNumbers": ["333282"],
    "scrapeDetailPage": true,
    "scrapeComplaints": true
}
```

Returns the full record including bond details and any complaint history from the last 2 years.

#### Active residential contractors in Scottsdale (lead gen)

```json
{
    "cities": ["Scottsdale"],
    "licenseStatus": "ACTIVE",
    "maxResultsPerSearch": 200,
    "scrapeDetailPage": false
}
```

Set `scrapeDetailPage: false` first to preview the data cheaply. Enable it for your final run once you've confirmed the results look right.

#### Commercial drywall contractors (by classification)

```json
{
    "cities": ["Phoenix", "Tucson"],
    "licenseType": "COMMERCIAL",
    "licenseClassification": "CR-10",
    "licenseStatus": "ACTIVE",
    "maxResultsPerSearch": 500
}
```

***

### Output schema

Each record is a flat JSON object pushed to the default Apify Dataset.

> **One record per license, not per company.** A contractor holding 3 classification codes (e.g. CR-8, CR-11, CR-42) will appear as 3 separate records, each with a different `primaryClassification`. This is how AZ ROC issues licenses — one per trade. Use `licenseId` to group records belonging to the same business if needed.

#### List-only record (`scrapeDetailPage: false`)

```json
{
    "licenseId": "a0o8y0000004CQAAA2",
    "licenseNumber": "333282",
    "businessName": "Grey Wolf Drywall LLC",
    "dbaName": null,
    "licenseType": "RESIDENTIAL",
    "classificationPrefix": "CR",
    "primaryClassification": "CR-10",
    "classificationDesc": "Drywall",
    "qualifyingParty": "Alfonso Lopez",
    "personnel": [
        { "name": "Alfonso Lopez", "roles": ["Member"] },
        { "name": "Alfonso Lopez", "roles": ["Qualifying Party"] }
    ],
    "licenseStatus": "Active",
    "city": "Waddell",
    "state": "AZ",
    "zip": "85355",
    "phone": "(602) 317-1895",
    "profileUrl": "https://azroc.my.site.com/AZRoc/s/contractor-search?licenseId=a0o8y0000004CQAAA2",
    "searchType": "LICENSE_NUMBER",
    "searchQuery": "333282",
    "scrapedAt": "2026-05-30T03:34:36.125Z"
}
```

#### Not-found record (license number not in database)

```json
{
    "licenseNumber": "999999",
    "found": false,
    "searchSucceeded": true,
    "searchType": "LICENSE_NUMBER",
    "searchQuery": "999999",
    "scrapedAt": "2026-05-30T03:34:36.125Z"
}
```

`searchSucceeded: true` means the portal confirmed 0 results — the license number is likely invalid or doesn't exist. `searchSucceeded: false` means the portal failed to load for this request (proxy issue, rate limiting). If you see `searchSucceeded: false` on a license you know exists, re-run that number before treating it as invalid.

#### Full record (`scrapeDetailPage: true`)

Adds the following fields:

| Field | Description |
|---|---|
| `entityType` | Corporation, LLC, Sole Proprietor, etc. |
| `issuedDate` | `YYYY-MM-DD` — when the license was originally issued |
| `renewedThroughDate` | `YYYY-MM-DD` — current license expiration |
| `classifications` | Array of all classification codes + descriptions for this business |
| `bondType` | Type of surety bond |
| `bondStatus` | `ACTIVE` or `INACTIVE` |
| `bondAmount` | Dollar amount (number) |
| `bondCompany` | Surety bond company name |
| `bondNumber` | Bond certificate number |
| `bondEffectiveDate` | `YYYY-MM-DD` |
| `bondExpirationDate` | `YYYY-MM-DD` |
| `openCases` | Currently open complaint cases |
| `disciplinedCases` | Disciplined complaint cases |
| `resolvedCases` | Resolved / settled cases |
| `complaintCount` | Total of all complaint types |
| `complaints` | Array of individual complaint records (last 2 years only) |

#### New in v1.1

- **`licenseType`** — `RESIDENTIAL`, `COMMERCIAL`, or `DUAL`, inferred from the classification code prefix (`CR-` = Residential, `C-` = Commercial, `B-` = Commercial, `A-` = Dual, etc.)
- **`searchType` / `searchQuery`** — which input produced this record (useful in batch runs)
- **`found: false`** — explicit not-found record for unmatched license numbers
- **`personnel.roles`** — array of role strings per person (was a semicolon-joined string)
- **City normalization** — `"PHOENIX"` and `"phoenix"` both become `"Phoenix"`
- **Phone normalization** — all formats unified to `(602) 317-1895`

***

### Performance

| Scenario | Approx time per result | Notes |
|---|---|---|
| License number lookups | ~35–45s per number | Includes proxy rotation; use `maxConcurrency: 3` to parallelize |
| Company / QP search | ~30–60s per search | Small result sets are faster |
| City search (small, < 50 results) | ~60–120s | One page |
| City search (large, 200+ results) | ~5–10 min | Multiple paginated pages |

> **Cost tip:** `scrapeDetailPage: false` is ~2–3× faster and cheaper. Use it for lead-gen list-building; enable it only when you need bond and complaint data.

***

### Proxy recommendation

The AZ ROC portal is hosted on Salesforce with standard bot protection. **Residential proxies are strongly recommended** for production runs. Datacenter IPs may work for small test runs but are not reliable at scale.

The default proxy config (`RESIDENTIAL` group) is the safest choice.

***

### Limitations

- **Complaint history** shows only the **prior two years** from the portal.
- **No official API** — all data is parsed from the rendered Salesforce LWC DOM. Major Salesforce updates may require selector adjustments.
- **City search is text-based** — searches for "Tempe" will match any record containing that string, including company names. Filter the output `city` field for precise location filtering.
- **Status / classification filters are post-scrape** — they do not narrow the portal's server-side search, only the records returned to you.
- **Rate limiting** — Salesforce may throttle aggressive crawls. `maxConcurrency ≤ 5` is recommended.
- **License not found** — if a license number returns no results, a `{ found: false }` record is pushed so you always have a complete accounting of your input list.

***

### FAQ

**Q: I searched for 5 license numbers and only got 3 records — what happened to the other 2?**
Those 2 license numbers were not found in the AZ ROC database. They will appear in your dataset as `{ "licenseNumber": "...", "found": false }` records. This could mean the number is invalid, the license was purged, or you have a typo. Check the AZ ROC portal directly at [azroc.my.site.com](https://azroc.my.site.com/AZRoc/s/contractor-search) to confirm.

**Q: I searched for `cities: ["Tempe"]` and got contractors from Tucson — is it broken?**
No — the AZ ROC portal uses a single text-search field. Searching for "Tempe" matches any record containing that word, including company names like "Temperature Control." For a true city filter, run a broader search and then filter the output dataset's `city` field to `"Tempe"` after the fact.

**Q: My status filter (`licenseStatus: "ACTIVE"`) isn't narrowing results — why?**
The filter is applied post-scrape: the actor collects all results the portal returns for your search, then filters by status before pushing to the dataset. If you're seeing expired records, it may be that the portal returned them before the filter was applied. Double-check your input and re-run with a smaller `maxResultsPerSearch` to inspect the raw results.

**Q: How much does a full city scrape cost?**
With `scrapeDetailPage: false`, a city returning 500 records costs $2.00 (500 × $4.00/1000). With `scrapeDetailPage: true`, add roughly 500 more page loads, bringing it to ~$4.00 total. Always set `maxResultsPerSearch` on your first run to avoid surprises.

**Q: What classification code should I use for plumbers / electricians / roofers?**
See the classification table above. Quick reference: Plumbing = `CR-37` / `C-37`, Electrical = `CR-11` / `C-11`, Roofing = `CR-42` / `C-42`, HVAC = `CR-39` / `C-39`. Note: `CR-` is residential, `C-` is commercial — run both if you need all license types for a trade.

**Q: Why do some records show a Qualifying Party that doesn't match my search?**
When you search by QP name, the AZ ROC portal matches against all personnel on a license, not just the Qualifying Party field. A license where "Garcia" is listed as an Owner or Member will appear in results for `qualifyingPartyNames: ["Garcia"]`, even if the QP is someone else. The `qualifyingParty` field in the output shows who the actual QP is — filter your dataset on that field to narrow results.

**Q: My run timed out before finishing — what do I do?**
The actor's default timeout is 3600 seconds (1 hour), but this can be overridden by your Apify plan's limits. For large jobs (many license numbers with detail pages, or broad city searches), go to the Run panel → Options → Timeout and set it explicitly to 3600s or higher. Detail-page scraping takes ~60–90s per record — plan your run size accordingly.

***

### Technical architecture

```
Actor Input
  │
  ▼
main.js — builds one job per search term
  │
  ▼
PlaywrightCrawler (Crawlee + fingerprint generator)
  │
  ├─► SEARCH requests (routes.js)
  │     ├─ Navigate to AZ ROC portal (waitUntil: domcontentloaded)
  │     ├─ Wait for Search button to appear (up to 45s for LWC to render)
  │     ├─ Fill search field (getByLabel strategy + placeholder fallback)
  │     ├─ Submit → wait for results table
  │     ├─ Set page size to 50 (dispatch native change event on <select>)
  │     │
  │     └─► Pagination loop
  │           ├─ Parse table HTML → parser.js
  │           │     ├─ Rowspan + separator-row handling
  │           │     ├─ licenseType inference from classification prefix
  │           │     ├─ City toTitleCase normalization
  │           │     └─ Phone format normalization
  │           ├─ Stamp searchType + searchQuery on each record
  │           ├─ Push records or enqueue DETAIL requests
  │           ├─ If LICENSE_NUMBER and 0 results → push found:false record
  │           ├─ Locate Next button (button.slds-button.right-btn)
  │           └─ Repeat until disabled or maxResultsPerSearch reached
  │
  └─► DETAIL requests (routes.js)
        ├─ Navigate to contractor profile URL
        ├─ Parse detail page → parser.js (data-label, dt/dd, SLDS form elements)
        └─ Actor.pushData() — merged base + detail record
```

***

### Changelog

#### v1.3.0 (current)

- **Fix:** `licenseClassification` filter now enforced post-scrape — only records matching the requested code are pushed (was previously ignored)
- **Fix:** `KB*` licenses (KB-1, KB-2) now correctly typed as `DUAL` instead of `COMMERCIAL`
- **Fix:** `personnel.name` values now normalized (Title Case, acronyms preserved) — matches `qualifyingParty` casing
- **New:** `classificationPrefix` field on every record (raw prefix like `CR`, `C`, `B`, `KB`) for users who want to audit the `licenseType` inference
- **New:** `searchSucceeded` field on `found: false` records — `true` = portal confirmed 0 results (likely invalid license), `false` = portal failed to load (transient network error, re-run before concluding)
- README: one-record-per-license callout; `searchSucceeded` explained in not-found schema

#### v1.2.0

- **Fix:** `scrapeDetailPage: true` no longer overwrites good list-level fields (businessName, licenseStatus, phone, personnel) with null detail-page values — only non-null detail fields are merged
- **Fix:** `businessName` now split correctly when portal concatenates DBA — "Acme LLC DBA : Trade Name" → `businessName: "Acme LLC"`, `dbaName: "Trade Name"`
- **Fix:** `licenseStatus` filter now enforced post-scrape — Expired/Inactive records are removed from output when you request ACTIVE only
- `qualifyingParty` and all-caps `businessName` values now normalized to Title Case
- `dbaName` now populated at list level (was only available from detail page)
- Classification code table added to README with common trade codes
- FAQ expanded: QP mismatch explanation, timeout guidance, corrected Roofing code (CR-42, not CR-39)
- `actor.json` version bumped to 1.1

#### v1.1.0

- `licenseType` field inferred from classification prefix (RESIDENTIAL / COMMERCIAL / DUAL)
- `searchType` and `searchQuery` fields on every record for batch traceability
- `found: false` record pushed for license numbers not in the AZ ROC database
- `personnel.roles` is now an array (was semicolon-joined string)
- City names normalized to Title Case (`"PHOENIX"` → `"Phoenix"`)
- Phone numbers normalized to `(NXX) NXX-XXXX` format
- `maxResultsPerSearch` default changed from 0 (unlimited) to 100
- README: corrected performance estimates, added city search warning, added filter behavior notes, added FAQ

#### v1.0.0

- Initial release: license number, company name, qualifying party, and city search
- Full pagination with class-based Next button detection
- Rowspan-aware results table parser
- Residential proxy defaults

# Actor input Schema

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

AZ ROC license numbers to look up directly (fastest mode). Leading zeros are added automatically. Example: \["333282", "12345"]

## `companyNames` (type: `array`):

Business names to search. Partial matching is supported. Example: \["Desert HVAC", "Acme Plumbing"]

## `qualifyingPartyNames` (type: `array`):

Individual qualifying party names to search. Example: \["John Smith"]

## `cities` (type: `array`):

Arizona city names to search. Large cities (Phoenix, Tucson) can yield thousands of results across many pages — use filters below to narrow. Example: \["Scottsdale", "Mesa"]

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

Filter results by license type. Applied to all search jobs.

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

Filter results by license status. Note: the AZ ROC portal uses a single text-search field, so this filter is applied post-scrape to the collected records rather than server-side. Results may include other statuses that are then filtered from the output.

## `licenseClassification` (type: `string`):

Filter by classification code (e.g. B-1, CR-39, C-37). Find all codes at https://roc.az.gov/license-classifications. Note: this filter is applied post-scrape — the portal's search is text-based and may return broader results, which are then narrowed in the output. Leave blank for all classifications.

## `maxResultsPerSearch` (type: `integer`):

Maximum records to collect per search job. 0 = unlimited (paginates all pages — use with caution on city searches, which can return thousands of records and unexpected billing). Default 100 is a safe starting point.

## `resultsPerPage` (type: `integer`):

Rows per page in the AZ ROC results table. Accepts 10, 20, or 50. Larger values mean fewer page-turn clicks — 50 is fastest.

## `scrapeDetailPage` (type: `boolean`):

Visit each contractor's detail page for full data: entity type, bond info, complaint history, all classifications, and personnel. Roughly doubles cost and runtime (~1–2 extra page loads per record). For large runs with detail pages enabled, set a longer Timeout in the Run options panel (recommended: 3600s). Recommended: start with false to preview data cheaply, then enable for your final run.

## `scrapeComplaints` (type: `boolean`):

Include the array of individual complaint records in output. Only applies when 'Scrape detail page' is enabled.

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

Residential proxies are strongly recommended for production runs. The AZ ROC portal uses Salesforce bot protection which can challenge datacenter IPs.

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

Parallel browser tabs. Higher = faster but more likely to trigger rate limiting. Keep ≤ 5 for residential proxies.

## Actor input object example

```json
{
  "licenseNumbers": [],
  "companyNames": [],
  "qualifyingPartyNames": [],
  "cities": [],
  "licenseType": "ALL",
  "licenseStatus": "ALL",
  "licenseClassification": "",
  "maxResultsPerSearch": 100,
  "resultsPerPage": 50,
  "scrapeDetailPage": true,
  "scrapeComplaints": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxConcurrency": 3
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "licenseNumbers": [],
    "companyNames": [],
    "qualifyingPartyNames": [],
    "cities": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ws_tony/az-roc-license-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "licenseNumbers": [],
    "companyNames": [],
    "qualifyingPartyNames": [],
    "cities": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ws_tony/az-roc-license-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "licenseNumbers": [],
  "companyNames": [],
  "qualifyingPartyNames": [],
  "cities": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call ws_tony/az-roc-license-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=ws_tony/az-roc-license-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Arizona ROC Contractor License Scraper",
        "description": "Scrape the Arizona Registrar of Contractors (AZ ROC) public license database. Search by license number, company name, qualifying party, or city. Returns license status, bond details, complaint history, and full contractor info. $4.00 per 1,000 results.",
        "version": "1.0",
        "x-build-id": "yMAf6drHeKkuhQkaR"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ws_tony~az-roc-license-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ws_tony-az-roc-license-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ws_tony~az-roc-license-scraper/runs": {
            "post": {
                "operationId": "runs-sync-ws_tony-az-roc-license-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ws_tony~az-roc-license-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-ws_tony-az-roc-license-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "licenseNumbers": {
                        "title": "License numbers",
                        "type": "array",
                        "description": "AZ ROC license numbers to look up directly (fastest mode). Leading zeros are added automatically. Example: [\"333282\", \"12345\"]",
                        "items": {
                            "type": "string"
                        }
                    },
                    "companyNames": {
                        "title": "Company / business names",
                        "type": "array",
                        "description": "Business names to search. Partial matching is supported. Example: [\"Desert HVAC\", \"Acme Plumbing\"]",
                        "items": {
                            "type": "string"
                        }
                    },
                    "qualifyingPartyNames": {
                        "title": "Qualifying party names",
                        "type": "array",
                        "description": "Individual qualifying party names to search. Example: [\"John Smith\"]",
                        "items": {
                            "type": "string"
                        }
                    },
                    "cities": {
                        "title": "Arizona cities",
                        "type": "array",
                        "description": "Arizona city names to search. Large cities (Phoenix, Tucson) can yield thousands of results across many pages — use filters below to narrow. Example: [\"Scottsdale\", \"Mesa\"]",
                        "items": {
                            "type": "string"
                        }
                    },
                    "licenseType": {
                        "title": "License type filter",
                        "enum": [
                            "ALL",
                            "RESIDENTIAL",
                            "COMMERCIAL",
                            "DUAL"
                        ],
                        "type": "string",
                        "description": "Filter results by license type. Applied to all search jobs.",
                        "default": "ALL"
                    },
                    "licenseStatus": {
                        "title": "License status filter",
                        "enum": [
                            "ALL",
                            "ACTIVE",
                            "SUSPENDED",
                            "EXPIRED",
                            "REVOKED",
                            "CANCELLED"
                        ],
                        "type": "string",
                        "description": "Filter results by license status. Note: the AZ ROC portal uses a single text-search field, so this filter is applied post-scrape to the collected records rather than server-side. Results may include other statuses that are then filtered from the output.",
                        "default": "ALL"
                    },
                    "licenseClassification": {
                        "title": "Classification code filter",
                        "type": "string",
                        "description": "Filter by classification code (e.g. B-1, CR-39, C-37). Find all codes at https://roc.az.gov/license-classifications. Note: this filter is applied post-scrape — the portal's search is text-based and may return broader results, which are then narrowed in the output. Leave blank for all classifications.",
                        "default": ""
                    },
                    "maxResultsPerSearch": {
                        "title": "Max results per search",
                        "minimum": 0,
                        "maximum": 50000,
                        "type": "integer",
                        "description": "Maximum records to collect per search job. 0 = unlimited (paginates all pages — use with caution on city searches, which can return thousands of records and unexpected billing). Default 100 is a safe starting point.",
                        "default": 100
                    },
                    "resultsPerPage": {
                        "title": "Results per page",
                        "minimum": 10,
                        "maximum": 50,
                        "type": "integer",
                        "description": "Rows per page in the AZ ROC results table. Accepts 10, 20, or 50. Larger values mean fewer page-turn clicks — 50 is fastest.",
                        "default": 50
                    },
                    "scrapeDetailPage": {
                        "title": "Scrape detail page",
                        "type": "boolean",
                        "description": "Visit each contractor's detail page for full data: entity type, bond info, complaint history, all classifications, and personnel. Roughly doubles cost and runtime (~1–2 extra page loads per record). For large runs with detail pages enabled, set a longer Timeout in the Run options panel (recommended: 3600s). Recommended: start with false to preview data cheaply, then enable for your final run.",
                        "default": true
                    },
                    "scrapeComplaints": {
                        "title": "Include complaint history",
                        "type": "boolean",
                        "description": "Include the array of individual complaint records in output. Only applies when 'Scrape detail page' is enabled.",
                        "default": true
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Residential proxies are strongly recommended for production runs. The AZ ROC portal uses Salesforce bot protection which can challenge datacenter IPs."
                    },
                    "maxConcurrency": {
                        "title": "Max concurrency",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Parallel browser tabs. Higher = faster but more likely to trigger rate limiting. Keep ≤ 5 for residential proxies.",
                        "default": 3
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
