# Networx Contractor Scraper — Home Service Pro Leads (`scrapersdelight/networx-contractor-scraper`) Actor

One row per US home-service contractor from networx.com: business name, trade, street address where published, city/state/ZIP, lat/lon, website, star rating, reviews, years in business, BBB accreditation and business highlights. 41 trades x 51 states, or sweep all 103,876 profiles.

- **URL**: https://apify.com/scrapersdelight/networx-contractor-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (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

$3.50 / 1,000 per contractor returneds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Networx Contractor Scraper — Home Service Pro Leads

Home-service contractors from [networx.com](https://www.networx.com), one row per contractor:
**businessName, trade, street, city, state, zip, latitude, longitude, website, rating,
reviewCount, yearsInBusiness, isClaimed, isBbbAccredited, bbbProfileUrl, businessHighlights,
awards, socialLinks** and the Networx `companyId` you can join on. Filter by trade, state and
city — or paste your own profile URLs.

**103,876 US contractor profiles, 41 trades × 51 states, enumerated live from Networx's own
sitemaps — not quoted from a marketing page.** Rows come back geocoded: **city, ZIP and
latitude/longitude on 100% of the 350 rows measured through trade + location mode** (96.5% on the
whole-directory route).

No login. No cookies. No CAPTCHA solving. No browser — it reads server-rendered HTML.

### Quick start

This is the input the Actor ships with. Click **Try for free**, hit **Start**, get rows without
typing anything:

```json
{
  "discovery": "trade",
  "trades": ["roofers"],
  "states": ["TX"],
  "cities": ["austin", "san-antonio", "houston", "dallas", "fort-worth", "el-paso"],
  "maxItems": 50
}
```

That exact input, run on **2026-08-12**: 6 listing pages → 97 listing rows → 77 unique
contractors → **50 records in 29 seconds**, 54 profile pages fetched, 0 failures. Cost: **$0.175**.

Or from the API:

```bash
curl -X POST "https://api.apify.com/v2/acts/ScrapersDelight~networx-contractor-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"trades":["plumbers"],"states":["TX"],"cities":["austin"],"maxItems":100}'
```

### The wedge: trade + street address + lat/lon, on a directory nobody else scrapes

As of **2026-08-12**, an Apify Store search for `networx` and `networx.com` returns **no other
Actor targeting networx.com**. That is the first half of the wedge. The second half is what the
site uniquely holds and how this Actor gets at it:

- **A normalised trade.** A Networx profile URL is just `/c.{business-slug}` — it carries no trade
  and no location, and the profile page never prints a normalised trade either ("Main Services" is
  free text the contractor typed). The trade exists in exactly one place: the listing-page URL. So
  trade + location mode walks `/{trade}/{state}/{city}` and hands you `trade` and `tradeSlug` as
  real, filterable values instead of a keyword guess.
- **A street address.** `street` is published **only** on the listing page, never on the profile.
  Fill is low and honest — 7.3% on the 300-row sample, 16.0% on the 50-row default run — but where
  it exists, nothing else on this site gives it to you.
- **Coordinates on every row.** `latitude`/`longitude` came back on 100% of the 300-row trade
  sample and 96.5% of the 200-profile directory sample, which is what makes territory mapping and
  radius assignment possible without a geocoding bill.
- **The whole directory, exactly once.** The company sitemap enumerates 103,876 profiles with
  **0.0% duplicates** — a complete-census option, not a search-results skim.

**Who this is for, stated plainly: this is an enrichment base, not a call sheet.** Name + trade +
geo + website, cheap and complete, that you point an e-mail finder or a licence lookup at. If you
need a phone number in the row you buy, read the next section before you spend anything.

### Read this before you buy rows

1. **Networx publishes no phone numbers and no licence records. At all.** Networx is a lead
   broker: the contractor's number lives behind its own quote form, and it holds no licence data.
   A scan of 200 random live profiles found **zero** `tel:` links, zero phone microdata and zero
   licence markup. There is no `phone` field and no `license` field, because nothing on this
   source can produce one.
2. **The one thing recovered anyway is labelled so you can never mistake it.**
   `phoneFromDescription` parses a NANP number out of the contractor's own free-text description
   when they typed one there. Fill: **9.5%** of all directory profiles, 5.3% of the 300-row trade
   sample, 6.0% of the 50-row default run. `emailFromDescription` works the same way (~1%
   directory, 2.0% on the default run). These are the contractor's own published words, not a
   Networx field.
3. **Only 13.0% of the directory is a claimed listing**, and the other 87% are thin stubs Networx
   built itself. Website fill is 42.3% on claimed listings and **0.0%** on unclaimed ones. That
   single fact explains why the two fill tables below disagree so violently, and it is what
   `claimedOnly` is for.
4. **The address is the contractor's HQ, not the city you searched.** Networx lists multi-state
   firms on local pages: 4 of the 50 rows in the default Texas run were headquartered in CA, WA
   and OR (8%). Filter on `city`/`state` after the fact if you need strictly local firms.
5. **Listing pages repeat the same firms across neighbouring towns.** Measured 77.5% duplicate
   rows on a 40-page contiguous Texas walk. The Actor drops them **before it fetches anything**,
   so you are never billed twice — but it means a 6-city run yields far fewer than 6 × 19 firms.

### What you get — one row per contractor

Every row carries every key. A field Networx did not publish is `null`, never a missing key.

| Group | Fields |
|---|---|
| **Identity** | `profileUrl`, `profileSlug`, `companyId`, `businessName` |
| **Trade** | `trade` (e.g. `Roofing`), `tradeSlug` (e.g. `roofers`), `mainServices` (free text), `description` |
| **Location** | `street`, `city`, `state`, `zip`, `latitude`, `longitude` |
| **Contact** | `website`, `additionalWebsites[]`, `socialLinks` (object: facebook, instagram, twitter, linkedin, youtube, pinterest), `phoneFromDescription`, `emailFromDescription` |
| **Reputation** | `rating` (0–5), `reviewCount`, `reviewsOnPage`, `awards`, `businessHighlights[]` |
| **Credentials** | `isClaimed`, `isBbbAccredited`, `bbbProfileUrl`, `yearsInBusiness`, `startYear` |
| **Media** | `logoUrl`, `categoryImage` |
| **Provenance** | `listingUrl` (the page it was discovered on), `sourceUrl`, `scrapedAt` (ISO-8601 UTC) |

`companyId` is Networx's own internal client id. It is stable across business-name and slug
changes, so use it — not the URL — as your join key when you re-scrape.

### Sample row

A real row from the default run above, captured **2026-08-12** (`scrapedAt` is UTC, so it reads
`08-13`). Every value is exactly as returned; the only edits are the truncated `description` and
the keys grouped into blocks for readability:

```jsonc
{
  "profileUrl": "https://www.networx.com/c.kas-contracting-company-inc",
  "profileSlug": "kas-contracting-company-inc",
  "companyId": 138423,
  "businessName": "Kas Contracting Company, Inc.",

  "trade": "Roofing",
  "tradeSlug": "roofers",
  "mainServices": "Roofing, Insurance Reconstruction",
  "description": "Kas Contracting is a local roofing general contractor with emphasis in residential and commercial insurance recovery and restoration…",

  "street": "7901 Cameron Rd Suite 2-238",
  "city": "Austin",
  "state": "TX",
  "zip": "78754",
  "latitude": 30.366077,
  "longitude": -97.644142,

  "website": "http://www.kascontracting.com",
  "additionalWebsites": ["http://www.homeadvisor.com/rated.KasContractingCo.46216699.html"],
  "socialLinks": {
    "twitter": "https://www.x.com/kascontracting",
    "facebook": "https://www.facebook.com/kascontracting"
  },
  "phoneFromDescription": "(512) 428-6364",   // parsed out of the contractor's own blurb
  "emailFromDescription": null,

  "rating": 5,
  "reviewCount": 5,
  "reviewsOnPage": 5,
  "awards": "\"People Love Us On Yelp\" 2016, 2017, 2018 Thumbtack Best Of Roofing 2016",
  "businessHighlights": [
    "Family-owned & Operated", "Financing Options", "Free Consultation",
    "Free In-home Estimate", "Full Warranty", "Guaranteed Repairs",
    "Lifetime Guarantee", "Maintaining Distance", "Minority-owned & Operated",
    "No-contact Payments", "On-time Guarantee", "Parts & Labor Guarantee",
    "Protective Gear Used", "Video Appointments", "Workmanship Guarantee"
  ],

  "isClaimed": true,
  "isBbbAccredited": true,
  "bbbProfileUrl": "https://www.bbb.org/us/tx/austin/profile/roofing-contractors/kas-contracting-0825-1000104478",
  "yearsInBusiness": 14,
  "startYear": 2012,

  "logoUrl": "https://cdn50.networx.com/media/max/140x140/clp_5db05e59c5db4.png",
  "categoryImage": null,
  "listingUrl": "https://www.networx.com/roofers/tx/austin",
  "sourceUrl": "https://www.networx.com/c.kas-contracting-company-inc",
  "scrapedAt": "2026-08-13T01:39:33.502Z"
}
```

Fields people misread:

- `rating` is a **0–5 star** value, not a percentage. `rating: null` and `reviewCount: null`
  arrive together and mean Networx published no rating — 17 of the 50 default rows.
- `reviewsOnPage` is a **count of review cards rendered on the profile** (0–34 in the default run)
  and is `0`, not `null`, when there are none. `reviewCount` is the site's own headline total.
- `isBbbAccredited` is always `true`/`false`; `false` means "no BBB accreditation badge on the
  page", not "checked with the BBB". Where it is `true` there is also a `bbbProfileUrl` (12 of 12
  in the default run).
- `awards` is a **single free-text string** the contractor wrote, not an array.
- `street` is the listing page's address line. When Networx prints "Serving Austin, TX" instead of
  an address, `street` is `null`.

### Field fill — measured on three real samples

**Sample A — the default run.** 50 contractors, `roofers` across 6 Texas metros, 2026-08-12.
This is the exact prefilled input, so you can reproduce the run yourself for $0.175. Expect the
percentages to move a few points: 77 contractors are discovered and the first 50 to come back win
the slots, so which 50 you get depends on the order the fetches finish. A re-run of this same input
returned website 56.0%, rating 68.0%, `street` 18.0%.

| Field | Fill |
|---|---:|
| `businessName`, `companyId`, `trade`, `tradeSlug`, `city`, `state`, `zip`, **`latitude`/`longitude`**, `isClaimed`, `isBbbAccredited` | **100%** |
| `logoUrl` | 82.0% |
| `description` | 80.0% |
| `businessHighlights` | 70.0% |
| `mainServices` | 66.0% |
| **`rating`** / `reviewCount` | **66.0%** |
| `yearsInBusiness` / `startYear` | 64.0% |
| **`website`** | **54.0%** |
| `socialLinks` | 32.0% |
| `bbbProfileUrl` | 24.0% |
| `awards` | 22.0% |
| **`street`** | **16.0%** |
| `additionalWebsites` | 12.0% |
| `phoneFromDescription` | 6.0% |
| `emailFromDescription` | 2.0% |
| **`phone` (as a Networx field)** | **0.0%** |
| **`license`** | **0.0%** |

**Sample B — trade + location, at scale.** 300 contractors, 6 trades requested across 18 metros in
5 states, resolving to 163 HQ cities in 9 states:

| Field | Fill | Note |
|---|---:|---|
| `businessName`, `trade`, `tradeSlug`, `city`, `state`, `zip`, `latitude`/`longitude` | **100%** | |
| `isClaimed` | 99.3% claimed | listing pages surface active listings almost exclusively |
| `description` | 85.0% | |
| `logoUrl` | 84.7% | |
| `businessHighlights` | 77.0% | "Family-owned", "24/7 Emergency Service", "Se Habla Español" … |
| `mainServices` | 72.0% | free text the contractor wrote |
| `yearsInBusiness` / `startYear` | 70.3% | up to 124 years |
| **`rating`** / `reviewCount` | **64.7%** | |
| **`website`** | **62.3%** | |
| `socialLinks` | 37.0% | Facebook, Instagram, X, LinkedIn, YouTube, Pinterest |
| `awards` | 21.7% | |
| `bbbProfileUrl` | 20.0% | direct link to the BBB profile |
| `additionalWebsites` | 13.7% | |
| **`street`** | **7.3%** | only ever published on the listing page |
| `phoneFromDescription` | 5.3% | recovered from free text |

**Sample C — whole directory.** 200 random profiles drawn across the full sitemap, 36 states:

| Field | Fill |
|---|---:|
| `businessName`, `companyId`, `isClaimed` | 100% |
| `city`, `state`, `zip`, `latitude`/`longitude` | 96.5% |
| `description` | 90.5% |
| `yearsInBusiness` | 76.0% |
| `mainServices` | 69.5% |
| `rating` / `reviewCount` | 62.5% |
| `businessHighlights` | 45.0% |
| `socialLinks` | 17.0% |
| `phoneFromDescription` | 9.5% |
| `bbbProfileUrl` | 6.5% |
| **`website`** | **5.5%** |
| `street` | 0.0% |

**The headline that could mislead you: this is not a phone-number product, and whole-directory
mode is not a website product.** Plan against Sample A or B if you want contactable rows, and
against Sample C if you want a census.

#### Why B and C disagree so much

| | website | highlights | social | rating |
|---|---:|---:|---:|---:|
| Claimed (n=26 of 200) | **42.3%** | 80.8% | 38.5% | 80.8% |
| Unclaimed stub (n=174 of 200) | **0.0%** | 39.7% | 13.8% | 59.8% |

Website fill is **0% on unclaimed stubs**, and only 13.0% of the directory is claimed — that is
the entire explanation for 5.5% vs 62.3%. Trade + location mode lands on active listings;
whole-directory mode gives you everything including the stubs. Use `claimedOnly: true` if you
sweep the directory and want leads rather than a census.

**Ratings skew hard to 5.0.** 81% of the rated contractors in the 300-row sample sit at exactly
5.0, and 30 of the 33 rated rows (90.9%) in the 50-row default run did too. A `minRating` of 4.0+
removes far less than you would expect; what it *does* remove is every unrated contractor, which
is 35% of the 300-row sample (34% of the 50-row default run).

**Every run prints its own measured fill in the log**, so you never have to trust this page for
the slice you actually bought.

### How to run it

#### Trade + location (default) — a trade in named cities

Fastest and cheapest. Naming cities skips 65 MB of sitemap downloads and builds the listing URLs
directly.

```json
{
  "discovery": "trade",
  "trades": ["plumbers", "heating-ac"],
  "states": ["TX"],
  "cities": ["austin", "san-antonio", "houston"],
  "maxItems": 1000
}
```

2 trades × 1 state × 3 cities = 6 listing pages, deduped, then one profile fetch per unique firm.

#### Trade + location — a whole state

Leave `cities` empty and the Actor reads Networx's city sitemaps to discover every valid city page
for your trades. Slower to start (it downloads and filters the sitemaps) but complete.

```json
{ "discovery": "trade", "trades": ["roofers"], "states": ["TX", "OK"], "maxItems": 5000 }
```

#### Listing-only mode — a fast roster

`fetchProfileDetails: false` returns just what the listing page shows: name, trade, city/state,
street address, star rating and blurb — at roughly **6× fewer requests**. You lose ZIP, lat/lon,
website, review count, years in business, BBB, claimed status, highlights and social links. **The
price per contractor is identical**, so this buys speed, not money. The quality filters below all
read profile-page fields, so the Actor stops with an explanation rather than silently ignoring
them if you combine the two.

```json
{ "discovery": "trade", "trades": ["landscapers"], "states": ["FL"], "fetchProfileDetails": false, "maxItems": 2000 }
```

#### Whole directory — the census

```json
{ "discovery": "directory", "claimedOnly": true, "maxItems": 5000 }
```

103,876 profiles, 0.0% duplicates, no trade attribution. Filters here run **after** each page is
fetched, because a `/c.{slug}` URL carries neither state nor trade: a live run with `claimedOnly`
plus two states fetched **520 profile pages to keep 10 rows**. You are billed only for the rows
kept, but the runtime is real — the Actor logs an estimate before it starts.

#### Paste your own URLs

```json
{ "profileUrls": ["https://www.networx.com/c.kas-contracting-company-inc"] }
```

Overrides discovery entirely. Use it to refresh a list you already own.

#### Paging across runs

Discovery order is deterministic. After a `maxItems: 1000` run, set `skip: 1000` to continue where
it stopped instead of re-buying the same firms.

### Input

Fields appear here in the same order as the Console form.

| Field | Type | Default | What it does |
|---|---|---|---|
| `discovery` | select | `trade` | `trade` = walk `/{trade}/{state}/{city}` listing pages (real trade + street address). `directory` = walk the company sitemap (all 103,876 profiles, no trade) |
| `trades` | multi-select (41) | `["roofers"]` prefilled | Which trades to walk. Leave it empty in trade mode and you get the built-in SAMPLE (25 roofers in Austin + Houston, TX) instead of an error; ignored in directory mode. Crossed with states and cities |
| `states` | multi-select (51) | `["TX"]` prefilled | 50 states + DC. Empty = nationwide, which is slow in trade mode (418,027 listing pages) |
| `cities` | stringList | 6 TX metros prefilled | City names or slugs. Requires states in trade mode. Naming cities skips 65 MB of sitemaps |
| `profileUrls` | stringList | — | Paste `https://www.networx.com/c.{slug}` URLs; overrides everything above |
| `claimedOnly` | boolean | `false` | Keep only claimed listings — 13.0% of the directory, but where the data is. ~8 fetches per kept row in directory mode |
| `withWebsiteOnly` | boolean | `false` | Keep only rows with a website — 5.5% of the directory, 42.3% of claimed, 62.3% on the trade route |
| `minRating` | select | `0` (any) | Any floor also drops every unrated contractor — 35% of the 300-row sample |
| `minReviewCount` | number | `0` | Counts run 1–49; anything above ~5 cuts hard |
| `minYearsInBusiness` | number | `0` | Present on 76.0% of profiles; the rest are dropped when a floor is set |
| `fetchProfileDetails` | boolean | `true` | Off = listing fields only, ~6× fewer requests, same price per row |
| `maxItems` | number | `100` (50 prefilled) | Your hard cost cap. Duplicates never count against it. Raise it *and* the run timeout together — throughput is ~3–10 rows/second |
| `skip` | number | `0` | Cross-run paging over a deterministic discovery order |
| `maxConcurrency` | number | `5` | Tested optimum; clamped 1–10 |
| `proxyConfiguration` | proxy | Apify Proxy on | Leave it on — see the transport ladder below |

#### The 41 trade slugs

`air-duct-cleaning` · `alarm-companies` · `appliance-repair-service` · `asphalt-contractors` ·
`bathroom-remodelers` · `carpenters` · `carpet-cleaners` · `carpet-services` ·
`cleaning-services` · `concrete-contractors` · `countertop-installation` · `deck-builders` ·
`drywall-contractors` · `electricians` · `fence-companies` · `flooring-contractors` ·
`foundation-contractors` · `garage-doors` · `gutter-contractors` · `handyman` · `heating-ac` ·
`home-insulation` · `home-restoration` · `junk-removal` · `kitchen-remodelers` · `landscapers` ·
`lawn-services` · `mold-remediation` · `painters` · `pest-control` · `plumbers` ·
`power-washing` · `remodeling-contractors` · `roofers` · `siding-contractors` · `snow-removal` ·
`solar-contractors` · `tile-contractors` · `tree-service` · `wildlife-removal` ·
`window-contractors`

You pick these from a labelled dropdown in the Console ("HVAC / Heating & Cooling" → `heating-ac`)
— the slugs are here for API callers. Coverage is very uneven: `roofers` has 20,907 city pages,
`solar-contractors` 728. Thin trades in small states genuinely run out — Vermont gutter
contractors returned **4 firms from 160 listing pages** — and the Actor tells you it ran out
rather than quietly returning a short file.

### Pricing

**$0.0035 per contractor returned — $3.50 per 1,000.** Pay-per-event, charged on the
`contractor-scraped` event. No monthly platform fee from this Actor.

| Run | Rows | Cost |
|---|---:|---:|
| The default prefilled run | 50 | **$0.175** |
| One metro, one trade | 1,000 | **$3.50** |
| A state-wide trade sweep | 10,000 | **$35.00** |
| The entire Networx directory | 103,876 | **$363.57** |

What you are **not** charged for:

- Listing pages and sitemaps — they are discovery, not rows.
- Duplicates. The same firm appearing on five neighbouring towns' pages is deduplicated **before
  anything is fetched**, so your dataset and your bill never hold it twice.
- Profiles your filters throw away. `claimedOnly` on the whole directory fetched 520 pages for 10
  kept rows in a live run — you paid for 10.
- Retries and proxy budget. Networx 403s about 40% of datacentre exit IPs; the re-fetching that
  costs is ours, not yours.

Rows are charged **as they are pushed** (`Actor.pushData(items, 'contractor-scraped')`), so if you
hit a budget cap you get whole rows and a stop, not a half-billed dataset. `maxItems` is your hard
cost dial.

### Honest limits

- **No phone numbers. No licence records.** Not scraped, not scrapeable, not hidden behind an
  option — Networx does not publish them. 200 random live profiles, zero `tel:` links, zero
  licence markup. If you need a dialler list today, our
  [BBB Scraper](https://apify.com/ScrapersDelight/bbb-scraper) or
  [Houzz Pro Scraper](https://apify.com/ScrapersDelight/houzz-pro-scraper) publish phone numbers
  and this Actor does not. If you need licence status, use
  [CSLB Contractor Scraper](https://apify.com/ScrapersDelight/cslb-contractor-scraper) for
  California.
- **No e-mail addresses either**, beyond the ~1–2% of contractors who typed one into their own
  description (`emailFromDescription`). The intended path is `website` →
  [Decision-Maker Email Finder](https://apify.com/ScrapersDelight/decision-maker-email-finder).
- **`street` is 7.3–16.0%.** Networx prints "Serving Austin, TX" far more often than an address,
  and never prints one on the profile page at all. This is a site-side publishing gap, not a
  parsing bug — the same page shows the same thing in a browser.
- **`website` collapses to 5.5% in whole-directory mode** because 87% of the directory is
  unclaimed stubs with no website at all. Use `claimedOnly: true`, or use trade + location mode
  where fill is 54–62.3%.
- **Ratings are near-useless as a discriminator.** 81% of rated firms in the 300-row sample sit at
  exactly 5.0. Treat `reviewCount` (1–49 in the 200-profile sample) as the real signal.
- **HQ ≠ service area.** 8% of the default Texas run were national firms headquartered in CA, WA
  and OR. Networx does not publish a service-area polygon, so this Actor does not invent one.
- **Duplicates are a property of the source.** A 40-page contiguous Texas walk produced 77.5%
  repeat rows. They are dropped before billing, but they mean a city list yields fewer unique
  firms than page count × 19.
- **US only.** Networx is a US directory: 50 states plus DC, nothing else.
- **No login, no CAPTCHA solving, no browser automation.** It reads public server-rendered HTML.
  There is no mode that changes that.

### How it works

1. Reads `https://www.networx.com/robots.txt` and takes the `Sitemap:` line it declares — the
   sitemap index is read, never hardcoded, so a move is survivable.
2. From the index: `company0..2.xml` = 103,876 profile URLs (the complete directory),
   `city0..8.xml` = 418,027 `/{trade}/{state}/{city}` listing pages, `state0.xml` = 1,704
   `/{trade}/{state}` pages.
3. Trade mode harvests contractor links from listing pages in deterministic order, deduping by
   profile URL as it goes; directory mode takes the profile URLs straight from the sitemap.
4. Each profile is parsed from schema.org microdata plus a comment-delimited sidebar. There is no
   JSON API, no state blob and no JSON-LD on this site, so the parser is markup-anchored and the
   run aborts if too much of it stops matching.
5. Rows are pushed in batches, charged on push.

#### Transport ladder — measured through Apify, retries off

| Rung | Result |
|---|---|
| Direct, no proxy, concurrency 10, 200 URLs | 200/200 (100%) @ 37.2 req/s — *not a shippable rung; no Apify container has this IP* |
| Apify Proxy, rotating exit IP, concurrency 5 | 23/40 (57.5%) — 17× HTTP 403 |
| Apify Proxy, **one pinned session**, concurrency 5 | **40/40 (100%)** |
| Apify Proxy, pinned, **200 contiguous URLs** | **200/200 (100%)** @ 3.0 req/s ← sustained load |
| Apify Residential, fresh session per request | 38/40 (95%) @ 1.4 req/s |

Networx does not rate-limit and does not challenge — no Cloudflare, no CAPTCHA, no cookie, no
login. It **blocklists** exit IPs: roughly 40% of datacentre exits get an instant 403, and a good
one never degrades. So the Actor **pins one proxy session per worker and rotates that session only
when it is refused** — the opposite of the fresh-session-per-request pattern rate-limited sites
need. Across validation it fetched **304/304** and **520/520** profile pages with zero failures,
and the default run above fetched 54/54. Residential is not needed and is slower, so the default
is the cheap datacentre rung.

Running with the proxy off inherits whichever platform IP the run lands on, which is a coin flip.
It is allowed, and the Actor warns you.

#### Uniqueness and dedupe

Measured on **contiguous** page walks, not sparse samples:

- 40 consecutive Texas roofer city pages: **244 listing rows → 55 unique firms = 77.5%
  duplicates.**
- 18 large metros across 5 states: **19.2%.**
- The 6-metro default run: **20.6% of 97 listing rows** were repeats.

The mechanism is real, not a bug: listing pages hold ~19 contractors, do not paginate, and a small
town's page is mostly the same regional firms as the next town over. **The dedupe key is the
profile URL, and it is applied before any profile is fetched**, so the dataset — and your bill —
never holds the same contractor twice. The duplicate rate is printed in every run's log. Across
runs, use `skip` (discovery order is deterministic). Whole-directory mode has **0.0% duplicates**
by construction.

#### When a run stops early, and when it fails

A silent empty run is the canonical broken scraper, so this Actor refuses to produce one — but it
also refuses to turn a transient block or your own filter choice into a red FAILED run. It **ends
Succeeded with an explanatory run status message, and bills nothing**, when:

- the sitemap index or a child sitemap cannot be read (Networx 403s ~40% of datacentre exit IPs —
  re-run and the Actor pins a different exit);
- the company sitemaps list fewer than 50,000 profiles, or the listing sitemaps fewer than 100,000
  URLs (a shrunken sitemap is still never passed off as the directory — the run stops instead);
- every listing page parses to nothing, or nothing matched your trades/states;
- `skip` is past the end of the directory;
- it emits zero rows — the message tells you whether your filters, the proxy, the time limit or
  Networx is the cause.

Malformed input is **coerced, not fatal**: an unknown trade slug, a non-2-letter state, a
non-`/c.` profile URL or cities without their states are dropped with a warning naming the valid
values, and the run continues with what is left. Sending a completely empty input (`{}` — what an
API, MCP or agent caller sends when it doesn't fill the Console form) runs the documented **sample**
instead of erroring.

**Run time budget.** The Actor reads `ACTOR_TIMEOUT_AT` and stops taking new work about 15% of the
run span before the platform would kill it, flushes everything already fetched, and ends Succeeded
with *"Stopped early to stay inside the run time limit; emitted N contractors."* Enumeration
(sitemaps and listing pages) stops even earlier, at ~55% of the span, so there is always time left
to fetch profiles and emit rows. Measured: `discovery: "directory"`, `maxItems: 100000` at a 120-second
run timeout → **SUCCEEDED in 101–115 s with 437 / 498 / 911 rows** across three runs, never TIMED-OUT.

There is exactly **one** condition that still ends the run FAILED, because it is the one thing you
must not silently receive: more than 20% of fetched profile pages return HTTP 200 with no parseable
business name — a Networx markup change, not a degraded delivery.

Running out of candidates is a loud warning naming what to change, not a silent short file.

### Who buys this

- **Field-service software vendors (ServiceTitan, Jobber, Housecall Pro, Workiz)** — 41 trades ×
  51 states, geocoded, so territory-based rep assignment falls out of `latitude`/`longitude` with
  no geocoding bill.
- **Roofing and exteriors suppliers and manufacturer rep firms** — `roofers` alone spans 20,907
  city pages; `awards` names the manufacturer certifications ("GAF Certified Contractor").
- **Agencies selling websites and SEO to trades** — the **46% of the default run with no
  `website`** is a ready-made target list, and the 32% with social profiles tells you who is
  already marketing.
- **B2B e-mail prospectors** — `website` on 54–62.3% of trade-route rows is the domain you feed to
  a pattern-inference e-mail finder; `businessName` + `city` + `state` is the dedupe key against
  your CRM.
- **Insurance, bonding and lending to contractors** — `yearsInBusiness` (up to 124), `startYear`
  and `isBbbAccredited` segment established firms from one-year-old operations.
- **M\&A and PE roll-up analysts in home services** — the whole-directory mode is a 103,876-row
  census with a stable `companyId`, so you can diff two sweeps and see who appeared and who left.

### How this differs from our other trades Actors

| Actor | What it is | Why you would use it instead |
|---|---|---|
| [Houzz Pro Scraper](https://apify.com/ScrapersDelight/houzz-pro-scraper) | Houzz professionals by trade and city, $0.0009/pro | It publishes a **phone number**; Networx does not |
| [Thumbtack Scraper](https://apify.com/ScrapersDelight/thumbtack-scraper) | Thumbtack top pros by city, $0.0027/pro | Hires count, Top Pro badges and response time — buying-intent signals Networx has no equivalent for |
| [BBB Scraper](https://apify.com/ScrapersDelight/bbb-scraper) | bbb.org businesses by keyword + location, $0.0018/business | Phone, full street address and the A+→F letter grade. Networx gives you the `bbbProfileUrl` to join on |
| [CSLB Contractor Scraper](https://apify.com/ScrapersDelight/cslb-contractor-scraper) | All 243,555 licensed California contractors, $0.004/licence | Licence status, bond and workers' comp — the qualification layer Networx has none of |
| [Decision-Maker Email Finder](https://apify.com/ScrapersDelight/decision-maker-email-finder) | Domain → named decision-makers + e-mail, $0.0008/lead | **The chain partner.** Feed it the `website` column from this Actor |

The honest one-liner: **Networx is where you go for a trade-tagged, geocoded census of US home-service firms; it is not where you go for a phone number.**

### Automating it

- **Save as a Task** — set your trades, states and cities in the Console, click *Save as task*, and
  re-run the same territory with one click or one API call.
- **Schedule it** — Apify Schedules, e.g. `0 6 * * 1` for a Monday-morning refresh. Pair with
  `skip` to page deeper each week instead of re-buying the same firms.
- **Integrations** — the dataset is JSON/CSV/Excel out of the box, and Apify's Zapier, Make, n8n,
  webhook and REST integrations all read it. `companyId` is the stable upsert key for your CRM.

### FAQ

**Does this need an account, a login or a cookie?**
No. Networx serves the whole directory as public HTML. No login, no cookie, no CAPTCHA solving, no
browser automation.

**Does it return phone numbers?**
No. Networx publishes none — 200 random profiles, zero `tel:` links. The only numbers you will see
are in `phoneFromDescription` (6.0–9.5%), which contractors typed into their own blurb.

**Can I get e-mail addresses?**
Only `emailFromDescription` (~1–2%). The intended path is to take `website` (54–62.3% on the trade
route) and run [Decision-Maker Email Finder](https://apify.com/ScrapersDelight/decision-maker-email-finder)
at $0.0008/lead.

**Is the address the contractor's office or the city I searched?**
The office. 4 of the 50 rows in the default Texas run were headquartered in CA, WA and OR because
Networx lists national firms on local pages. Filter on `state` afterwards if that matters.

**Can I get the whole directory in one run?**
Yes — `discovery: "directory"`, `maxItems: 0` is not supported but `maxItems: 103876` is. That is
$363.57 and several hours at ~3 rows/second. Most buyers want a trade and a metro instead.

**Two runs — will I get duplicates?**
Not within a run (deduped on profile URL before fetching). Across runs, discovery order is
deterministic, so set `skip` to where the last run stopped. `companyId` is a stable upsert key
either way.

**Do I get charged for rows my filters remove?**
No. You are charged per row delivered. A live `claimedOnly` directory run fetched 520 profile pages
and billed 10 rows.

**Why did my run return fewer rows than I asked for?**
Because the trade × state you picked ran out of listings — Vermont gutter contractors is 4 firms
from 160 pages. The run logs a warning naming what to change; it never pads the file.

**Do I need a proxy? Do I need residential?**
Keep Apify Proxy on; you do not need residential. Networx 403s ~40% of datacentre exits, so the
Actor pins one session per worker and rotates only on refusal: 200/200 pages once pinned, versus
23/40 on a rotating IP. Residential measured 38/40 and is slower.

**Which trades and states work?**
All 41 trades and all 50 states plus DC, picked from labelled dropdowns. An unknown slug stops the
run with the full list of valid values rather than returning nothing.

**Is `minRating` worth setting?**
Rarely. 81% of rated firms sit at exactly 5.0, and any floor also removes the 35% of the file
with no rating at all.

**Will a run ever succeed with zero rows?**
No. Zero rows throws with the scope, the HTTP counts and a diagnosis. There is no "successful empty
run" on this Actor.

**Something looks wrong — how do I debug it?**
Every run logs the listing-page count, the duplicate rate, the profile-fetch count, the failure
counts, the proxy-session rotations and its own measured field fill. Compare that log against the
tables above; if they diverge, open an issue.

### Legal and fair use

Public directory data published by networx.com, read from the site's own `robots.txt` sitemaps.
`robots.txt` declares no crawl-delay and disallows only `/quote.php`, `/review/write/`,
`/article/search.php`, `/login`, `/privacy`, `/terms-use` and `/do-not-sell` — none of which this
Actor requests. It does not log in and collects nothing behind any authentication. Default
concurrency is 5.

Rows describe businesses, but many US home-service firms are sole traders, so a row can constitute
personal data. You are responsible for complying with networx.com's terms and with how you use the
data, including GDPR/CCPA where the records identify individuals and CAN-SPAM/TCPA for outreach.

Networx is a trademark of its owner; this Actor is not affiliated with or endorsed by them.

### Feedback

Found a missing field, or want a filter that is not here? Open an issue on the **Issues** tab.

# Actor input Schema

## `discovery` (type: `string`):

How contractors are found. "Trade + location" walks Networx's /{trade}/{state}/{city} listing pages — the only place a normalised trade and a street address exist — and dedupes the heavy overlap between neighbouring towns (77.5% repeats on a 40-page contiguous Texas walk, all dropped before you are billed). "Whole directory" walks the company sitemap instead: all 103,876 profiles, 0.0% duplicates, one fetch each, but with no trade attribution and with state/city filters applied only AFTER each page is fetched. Ignored when you paste profile URLs below.

## `trades` (type: `array`):

Which of Networx's 41 trades to walk. Pick as many as you like — each trade is crossed with each state and city, so 2 trades × 1 state × 3 cities = 6 listing pages. Required in "Trade + location" mode; ignored in "Whole directory" mode, which carries no trade at all. If you leave it empty in "Trade + location" mode the run falls back to a small built-in SAMPLE (roofers in Austin and Houston, TX, capped at 25 contractors) so an API or agent caller never gets an empty dataset. Coverage is very uneven: roofers has 20,907 city pages, solar-contractors 728, so a thin trade in a small state genuinely runs out (Vermont gutter contractors: 4 firms from 160 listing pages) and the Actor tells you it ran out rather than returning a short file quietly.

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

US states to search. All 50 plus DC are covered. Leave empty to sweep nationwide — slow in "Trade + location" mode, because there are 418,027 trade × city listing pages to walk. In "Whole directory" mode a state filter costs one profile fetch per contractor tested, because a /c.{slug} profile URL carries no location; you are only billed for the rows kept, but the runtime is real.

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

City names or Networx city slugs, e.g. austin, san-antonio, corpus-christi — spaces are converted for you, so "San Antonio" works. Naming cities makes the run much faster: the Actor builds the listing URLs directly instead of downloading 65 MB of sitemaps to discover them. In "Trade + location" mode cities REQUIRE their states, because "springfield" exists in 30+ of them; a city that does not exist in a given state is skipped with a warning naming the URL. Leave empty to sweep whole states.

## `profileUrls` (type: `array`):

Optional. Paste exact Networx company profile URLs (https://www.networx.com/c.{business-slug}) and discovery is skipped entirely — this overrides the discovery mode, trades, states and cities. Use it to re-scrape a list you already own, or to refresh yesterday's rows. A URL that is not a /c.{slug} profile stops the run with an error naming the first bad one, rather than silently returning nothing.

## `claimedOnly` (type: `boolean`):

Keep only contractors who have claimed their Networx listing rather than unclaimed directory stubs. Measured 13.0% of the directory — but that 13% is where the data is: website 42.3% vs 0.0%, business highlights 80.8% vs 39.7%, social links 38.5% vs 13.8%, rating 80.8% vs 59.8% (n=200 random profiles). Turning this on in "Whole directory" mode means roughly 8 profile pages fetched per row kept.

## `withWebsiteOnly` (type: `boolean`):

Keep only contractors whose profile lists a company website. This is the harsh one: 5.5% of all directory profiles carry a website (42.3% of claimed ones, 62.3% on the trade + location route). It is also the field you need if you plan to enrich for e-mail, because Networx publishes no phone numbers and no e-mail addresses at all.

## `minRating` (type: `string`):

Drop contractors rated below this. Setting any floor also drops every UNRATED contractor, and only 62.5% of directory profiles (64.7% on the trade + location route) carry a rating at all. Ratings also skew hard to the top — 88% of rated contractors sit at exactly 5.0 (81% in the 300-row trade + location sample) — so a 4.0+ floor removes far less than you would expect. "Any rating" keeps everyone.

## `minReviewCount` (type: `integer`):

Drop contractors with fewer verified Networx reviews than this. Review counts are small — the 200-profile sample ran 1 to 49 — so anything above ~5 cuts hard. 0 = keep everyone, including the 37.5% with no reviews.

## `minYearsInBusiness` (type: `integer`):

Drop contractors in business fewer years than this. Present on 76.0% of directory profiles (70.3% on the trade + location route), running up to 124 years; the ones with no founding year are dropped whenever a floor is set. 0 = keep everyone.

## `fetchProfileDetails` (type: `boolean`):

ON (recommended): fetch each contractor's profile page for ZIP, lat/lon, rating, review count, description, website, years in business, BBB accreditation, claimed status, business highlights and social links. OFF: return only what the listing page shows — name, trade, city/state, street address, star rating and blurb — at roughly 6× fewer requests. The price per contractor is the same either way, so this only buys speed; leave it on unless you specifically want a fast roster. Only applies to "Trade + location" mode, and the quality filters above stop working when it is off (the Actor tells you instead of silently ignoring them).

## `maxItems` (type: `integer`):

How many contractor records to return, and therefore your hard cost cap: 50 rows = $0.175, 100 rows = $0.35, 1,000 rows = $3.50. Duplicates are dropped before they are fetched and never count against it. Throughput is roughly 3 contractors/second through the Apify proxy, so raise the run timeout as well as this number when you want thousands.

## `skip` (type: `integer`):

Skip this many contractors before collecting. Discovery order is deterministic, so after a maxItems=1000 run set skip=1000 to continue where it stopped instead of re-buying the same firms. In "Whole directory" mode a skip past the end of the 103,876-profile directory stops the run with an error rather than returning nothing.

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

Parallel page fetches, one pinned proxy session each. 5 is the tested default — a 200-page sustained-load run at that setting returned 200 of 200 pages at 3.0 req/s. Networx does not rate-limit and does not throttle; it blocklists bad exit IPs instead, which the Actor handles by rotating that worker's session. Values are clamped to 1–10.

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

Leave on Apify Proxy (automatic) — that is the measured rung. Networx refuses roughly 40% of datacentre exit IPs with an instant HTTP 403 (no Cloudflare, no CAPTCHA, no cookie), so the Actor pins ONE proxy session per worker and rotates it only when that IP is refused: measured 200/200 pages once pinned, versus 23/40 on a rotating exit IP. Residential is not needed and is slower (38/40 at 1.4 req/s). Turning the proxy off inherits whichever platform IP the run lands on, which is a coin flip; the Actor allows it and warns you.

## Actor input object example

```json
{
  "discovery": "trade",
  "trades": [
    "roofers"
  ],
  "states": [
    "TX"
  ],
  "cities": [
    "austin",
    "san-antonio",
    "houston",
    "dallas",
    "fort-worth",
    "el-paso"
  ],
  "claimedOnly": false,
  "withWebsiteOnly": false,
  "minRating": "0",
  "minReviewCount": 0,
  "minYearsInBusiness": 0,
  "fetchProfileDetails": true,
  "maxItems": 50,
  "skip": 0,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per Networx contractor: business name, trade, street address where published, city/state/ZIP, lat/lon, website, rating, review count, years in business, claimed status, BBB accreditation, business highlights and social links.

# 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 = {
    "discovery": "trade",
    "trades": [
        "roofers"
    ],
    "states": [
        "TX"
    ],
    "cities": [
        "austin",
        "san-antonio",
        "houston",
        "dallas",
        "fort-worth",
        "el-paso"
    ],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/networx-contractor-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 = {
    "discovery": "trade",
    "trades": ["roofers"],
    "states": ["TX"],
    "cities": [
        "austin",
        "san-antonio",
        "houston",
        "dallas",
        "fort-worth",
        "el-paso",
    ],
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/networx-contractor-scraper").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 '{
  "discovery": "trade",
  "trades": [
    "roofers"
  ],
  "states": [
    "TX"
  ],
  "cities": [
    "austin",
    "san-antonio",
    "houston",
    "dallas",
    "fort-worth",
    "el-paso"
  ],
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/networx-contractor-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/networx-contractor-scraper"
        }
    }
}

```

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/6ON00dHzfcpLMxlqb/builds/uWov2mPa39wNJ1zyr/openapi.json
